Sign Up

Sign Up to our social questions and Answers Engine to ask questions, answer peopleโ€™s questions, and connect with other people.

Have an account? Sign In

Have an account? Sign In Now

Sign In

Login to our social questions & Answers Engine to ask questions answer peopleโ€™s questions & connect with other people.

Sign Up Here

Forgot Password?

Don't have account, Sign Up Here

Forgot Password

Lost your password? Please enter your email address. You will receive a link and will create a new password via email.

Have an account? Sign In Now

Sorry, you do not have permission to ask a question, You must login to ask a question.

Forgot Password?

Need An Account, Sign Up Here

Please type your username.

Please type your E-Mail.

Please choose an appropriate title for the post.

Please choose the appropriate section so your post can be easily searched.

Please choose suitable Keywords Ex: post, video.

Browse

Need An Account, Sign Up Here

Please briefly explain why you feel this question should be reported.

Please briefly explain why you feel this answer should be reported.

Please briefly explain why you feel this user should be reported.

Sign InSign Up

Querify Question Shop: Explore Expert Solutions and Unique Q&A Merchandise

Querify Question Shop: Explore Expert Solutions and Unique Q&A Merchandise Logo Querify Question Shop: Explore Expert Solutions and Unique Q&A Merchandise Logo

Querify Question Shop: Explore Expert Solutions and Unique Q&A Merchandise Navigation

  • Home
  • About Us
  • Contact Us
Search
Ask A Question

Mobile menu

Close
Ask a Question
  • Home
  • About Us
  • Contact Us
Home/ Questions/Q 2229

Querify Question Shop: Explore Expert Solutions and Unique Q&A Merchandise Latest Questions

Author
  • 61k
Author
Asked: November 26, 20242024-11-26T03:29:09+00:00 2024-11-26T03:29:09+00:00

Create beautiful rosette patterns with JavaScript ๐Ÿ–Œ๏ธ๐ŸŒผ

  • 61k

I mean, aren't they just beautiful to look at? I mean, look at them:

A rosette pattern Another rosette pattern

Amazing, aren't they? The symmetry, the complexity, the maths! Let's build something like this today! We'll use an SVG and some JS for that.

Wait, wait, wait! The maths?

Yup, the maths! In order to generate these beautiful patterns, we'll need some geometry. We're only going to define the radius of the circle of the rosette pattern, the amount of segments and a few other variables that contribute to the overall pattern. We'll derive the rest from that.

Let's start by analyzing how rosette patterns are structured.

The structure

The symmetry of such a circular pattern is given by its segments. The same segment is used over and over, by mirroring, rotating, mirroring again and rotating again and so on.

So to line up the individual elements of a segment, the circle needs to be cut into an even number of equally sized (meaning: same angle) slices, just like a cake.

The content of an individual segment doesn't matter. It can be anything – the mirroring guarantees that the borders between slices line up perfectly.

Now how does this help us with implementing a rosette pattern? We could create a single segment as an SVG and reuse it via SVGs handy <use> tag and some transform statements.

Since SVGs usually come in rectangular shapes only, we need some way to know the exact width and height of a segments. That's nothing geometry hasn't solved yet.

Creating a segment

In order to create a segment, we want to know the radius of the final rosette pattern and its angle. A segment is roughly speaking a triangular shape.

Let's do an example. If we want to slice a circle into, say, 4 equally sized slices, a single segment would basically look like this:

A circle with a the top left quarter of it marked with lines.

If we would like to create triangular shape out of that, we can extend the two lines, until we find the spot where we can draw a tangent line to the circle, forming a triangle:

Triangle over a circle, it's hypothenuse being a tangent to the circle.

(Side note: In this example, the triangle is already a 90 degrees triangle, but that only works with 4 segments, since 360ยฐ/ 4 = 90ยฐ.)

By moving the tangent over and connecting the dots, we get a full rectangle containing the segment:

Rectanglular shape layed over the circle

The height can be calculated with this formula:

hsegment=2โˆ—sin(ฮฑ/2)โˆ—r h_{segment} = 2 * sin(alpha / 2) * r hsegmentโ€‹=2โˆ—sin(ฮฑ/2)โˆ—r

With hsegmenth_{segment}hsegmentโ€‹ being the height, ฮฑalphaฮฑ being the angle of the segment (in this case: 90ยฐ) and rrr being the radius of the segment. This formula uses the fact that every triangle can be divided into two right angle triangles and that these triangles are similar if the the triangle has two sides of equal length.

The width can then be calculated using Pythagorases theorem:

wsegment=r2โˆ’(hsegment/2)โˆ—โˆ—2 w_{segment} = sqrt{r ^ 2 – (h_{segment} / 2) ** 2} wsegmentโ€‹=r2โˆ’(hsegmentโ€‹/2)โˆ—โˆ—2โ€‹

You may have noticed that we're not using the radius directly here. Calculating the width again from the radius and the height will make the triangle have the actual angle we want. Otherwise it would be a bit too narrow.

With the height and the width of the segment, we can now also calculate the final width and height of the entire rosette SVG using Pythagorases theorem:

hpattern=2โˆ—(hsegment/2)2+r2 h_{pattern} = 2 * sqrt{(h_{segment} / 2)^2 + r^2} hpatternโ€‹=2โˆ—(hsegmentโ€‹/2)2+r2โ€‹
wpattern=hpattern w_{pattern} = h_{pattern} wpatternโ€‹=hpatternโ€‹

Now we know how to get the size of a segment. Let's take care of its content!

Generating a segments content

We're gonne be a bit… cheap about that. Let's just use more circles! By randomly placing differently colored and differently sized circles into the rectangle and cutting them off at the edges of the triangle, we can create very interesting shapes and designs.

To select a bunch of colors that go well together, we will use a technique described in this Twitter thread:

The technique is rather straight-forward: Generate a random HSL color, add 75 (or any number, really) to its hue, chose random lightness and saturation values and you've got two colors! Repeat that with the second color to get a third one, and repeat a few more times until you've got the number of colors you want.

If you don't know how HSL colors work, this post I did a while ago has an in-depth explanation:

thormeier

Let's build a rainbow on a canvas from scratch! ๐ŸŒˆ๐Ÿ“

Pascal Thormeier ใƒป Sep 25 '21

#webdev #tutorial #javascript #mathematics

So far so good. I think we can start coding!

Let's code the foundation

Let's start with a rounding function and random number function, because JavaScripts Math.random is kind of bulky at times:

/**  * Rounds a number  * @param n Number to round  * @param places Number of places to round to  * @returns {number}  */ const round = (n, places) => Math.round(n * (10 ** places)) / (10 ** places)  /**  * Random number between min and max  * @param min Lower end of range  * @param max Upper end of range  * @param precision Number of decimal places  * @returns {*}  */ const rand = (min, max, precision = 0) => {   return round((Math.random() * (max - min) + min), precision) } 
Enter fullscreen mode Exit fullscreen mode

Next, we create a Color class and a function that creates a palette of a given size. I'll add a function to the Color class that gives me the next color.

/**  * Represents a color  */ class Color {   /**    * Constructor    * @param h Hue    * @param s Saturation    * @param l Lightness    */   constructor(h, s, l) {     this.h = h     this.s = s     this.l = l   }    /**    * Creates a random color    * @returns {Color}    */   static createRandom() {     return new Color(       rand(0, 360),       rand(25, 75),       rand(25, 75)     )   }    /**    * Generates the next color    * @param hueStepSize By how much the Hue value should change    * @returns {Color}    */   getNextColor(hueStepSize) {     let nextHue = this.h + hueStepSize      // Wrap around if hue is not between 0 and 360     if (nextHue < 0) {       nextHue += 360     } else if (nextHue > 360) {       nextHue -= 360     }      return new Color(       nextHue,       rand(25, 75),       rand(25, 75)     )   }    /**    * Get a string representation of this color    * @returns {string}    */   toString() {     return `hsl(${this.h}, ${this.s}%, ${this.l}%)`   } }  /**  * Creates a color palette of a given size.  * @param numberOfColors Number of colors.  * @param hueStepSize By how much the hue should change.  * @returns {*[]}  */ const getRandomColorPalette = (numberOfColors, hueStepSize) => {   const colors = []    let currentColor = Color.createRandom()   colors.push(currentColor)    while (numberOfColors > 0) {     currentColor = currentColor.getNextColor(hueStepSize)     colors.push(currentColor)     numberOfColors--   }    return colors } 
Enter fullscreen mode Exit fullscreen mode

Off for a good start. Next, we'll create a Circle class that represents part of the content of a single segment:

class Circle {   /**    * Represents a circle within a segment    * @param cx    * @param cy    * @param r    * @param color    */   constructor(cx, cy, r, color) {     this.cx = cx     this.cy = cy     this.r = r     this.color = color   }    /**    * Get a string representation of this circle    * @returns {string}    */   toString() {     return `<circle       cx="${this.cx}"       cy="${this.cy}"       r="${this.r}"       fill="${this.color.toString()}"       stroke="#000"       stroke-width="2"     />`   } } 
Enter fullscreen mode Exit fullscreen mode

Next up, we want to create a Segment class that can generate its own circles:

class Segment {   /**    * Represents a single Segment    * @param width Width of the segments rectangle    * @param height Height of the segments rectangle    * @param numberOfCircles Number of circles it should contain    * @param colorPalette The color palette used    */   constructor(width, height, numberOfCircles, colorPalette) {     this.width = width     this.height = height     this.circles = []      this.generateCircles(numberOfCircles, colorPalette)   }    /**    * Generates a given number of random circles with    * different colors from a given palette    * @param numberOfCircles Number of circles to generate    * @param colorPalette Palette to chose colors from    */   generateCircles(numberOfCircles, colorPalette) {     while (numberOfCircles > 0) {       // 5% to 25% of the segments width.       const radius = rand(this.width * 0.05, this.width * 0.25)        this.circles.push(new Circle(         // Width - radius guarantees that the circle doesn't overlap the width.         rand(0, this.width - radius),         rand(0, this.height),         radius,         colorPalette[rand(0, colorPalette.length - 1)]       ))        numberOfCircles--     }   }    /**    * Creates a string representation of this segment    * @returns {string}    */   toString() {     // ...   } } 
Enter fullscreen mode Exit fullscreen mode

Notice how I left out the toString method of the segment? I want to give this one some special attention. In order to cut out the actual segment, we'll use clippath. Remember the triangle from earlier? Its points align perfectly with the top right, bottom right and center left of the rectangle:

class Segment {   // ...    /**    * Creates a string representation of this segment    * @param id DOM id for referencing    * @returns {string}    */   toString(id) {     // This is used to "scale" the clippath a bit without using transform: scale     // When finished, there will be some artifacts at the borders, this reduces them.     const tolerance = 1      return `       <svg width="${this.width + tolerance}" height="${this.height + tolerance}" id="${id}">         <defs>           <clipPath id="triangle">             <!-- scaleZ(1) forces GPU rendering -->             <polygon transform="scaleZ(1)" points="               -${tolerance / 2},${this.height / 2}                ${this.width + (tolerance / 2)},-${tolerance / 2}                ${this.width + (tolerance / 2)},${this.height + (tolerance / 2)}"             />           </clipPath>         </defs>          <g style="clip-path: url(#triangle)">           ${this.circles.map(c => c.toString()).join("
")}         </g>       </svg>     `   }    // ... } 
Enter fullscreen mode Exit fullscreen mode

However, we added the tolerance variable. SVG's transform in combination with clippath adds some artifacts at the borders of the triangle. I haven't exactly figured out why this is happening, but enlarging the segment just a tiny little bit is already helping a lot.

Let's try that:

const segment = new Segment(   400, // width   200, // height   12, // number of circles   getRandomColorPalette(5, 25) )  const container = document.createElement('div') container.innerHTML = segment.toString('segment') document.body.appendChild(container) 
Enter fullscreen mode Exit fullscreen mode

And we get something like this:

A triangle with some randomly placed, randomly colored circles

Almost there! Now we only need to repeat the segment a few times.

Creating the full pattern

Next up, we need a class called Pattern that shows all the segments by rotating and mirroring them.

class Pattern {   /**    * Creates a full pattern    * @param numberOfSegments    * @param radius    */   constructor(numberOfSegments, radius) {     this.numberOfSegments = numberOfSegments     const angle = 360 / numberOfSegments     // The formula we used earlier.     // `angle * Math.PI / 180.0` is necessary, because Math.sin     // uses radians instead of degrees.     const segmentHeight = 2 * Math.sin((angle * Math.PI / 180.0) / 2) * radius      const segmentWidth = Math.sqrt(radius ** 2 - (segmentHeight / 2) ** 2)      const colorPalette = getRandomColorPalette(5, 25)      this.segment = new Segment(segmentWidth, segmentHeight, rand(5, 12),  colorPalette);      this.segmentHeight = this.segment.height     this.width = 2 * Math.sqrt((this.segment.height / 2) ** 2 + radius ** 2)     this.height = this.width   }    /**    * Creates a string representation of this pattern    * @returns {string}    */   toString() {     // ...   } } 
Enter fullscreen mode Exit fullscreen mode

To render the entire pattern, we first need to get the rotation working:

  /**    * Creates a string representation of this pattern    * @returns {string}    */   toString() {     const segments = []     let numberOfSegmentsLeft = this.numberOfSegments     while (numberOfSegmentsLeft > 0) {       // Rotate the segment       const rotationRadius = (360 / this.numberOfSegments * numberOfSegmentsLeft) % 360        let transformRotation = `rotate(${rotationRadius})`        segments.push(`         <use            href="#segment"           transform="${transformRotation} translate(${this.width / 2} ${this.width / 2 - this.segmentHeight / 2})"           transform-origin="${this.width / 2} ${this.width / 2}"         ></use>       `)        numberOfSegmentsLeft--     }      return `       <div>         ${this.segment.toString('segment')}       </div>        <div>         <svg width="${this.width}" height="${this.height}">           ${segments.join("
")}         </svg>       </div>     `   } 
Enter fullscreen mode Exit fullscreen mode

Now, to flip every second segment, we need to add a scale to the transform:

// ...       let transformRotation = `rotate(${rotationRadius})`       if (numberOfSegmentsLeft % 2 === 0) {         transformRotation += ' scale(1, -1)'       } // ... 
Enter fullscreen mode Exit fullscreen mode

The result

And here's the result:

And since everything's random, every pattern you get is unique and is only ever generated for you! If the one you see on load is boring, simply click on the “Show new” button to (hopefully) get a more beautiful one.


I hope you enjoyed reading this article as much as I enjoyed writing it! If so, leave a โค๏ธ or a ๐Ÿฆ„! I write tech articles in my free time and like to drink coffee every once in a while.

If you want to support my efforts, you can offer me a coffee โ˜• or follow me on Twitter ๐Ÿฆ! You can also support me directly via Paypal!

Buy me a coffee button

javascriptsvgtutorialwebdev
  • 0 0 Answers
  • 0 Views
  • 0 Followers
  • 0
Share
  • Facebook
  • Report

Leave an answer
Cancel reply

You must login to add an answer.

Forgot Password?

Need An Account, Sign Up Here

Sidebar

Ask A Question

Stats

  • Questions 4k
  • Answers 0
  • Best Answers 0
  • Users 1k
  • Popular
  • Answers
  • Author

    How to ensure that all the routes on my Symfony ...

    • 0 Answers
  • Author

    Insights into Forms in Flask

    • 0 Answers
  • Author

    Kick Start Your Next Project With Holo Theme

    • 0 Answers

Top Members

Samantha Carter

Samantha Carter

  • 0 Questions
  • 20 Points
Begginer
Ella Lewis

Ella Lewis

  • 0 Questions
  • 20 Points
Begginer
Isaac Anderson

Isaac Anderson

  • 0 Questions
  • 20 Points
Begginer

Explore

  • Home
  • Add group
  • Groups page
  • Communities
  • Questions
    • New Questions
    • Trending Questions
    • Must read Questions
    • Hot Questions
  • Polls
  • Tags
  • Badges
  • Users
  • Help

Footer

Querify Question Shop: Explore Expert Solutions and Unique Q&A Merchandise

Querify Question Shop: Explore, ask, and connect. Join our vibrant Q&A community today!

About Us

  • About Us
  • Contact Us
  • All Users

Legal Stuff

  • Terms of Use
  • Privacy Policy
  • Cookie Policy

Help

  • Knowledge Base
  • Support

Follow

© 2022 Querify Question. All Rights Reserved

Insert/edit link

Enter the destination URL

Or link to existing content

    No search term specified. Showing recent items. Search or use up and down arrow keys to select an item.