头图

"Front-end Daily Combat" No. 178: Floor Tile Pattern Designer

comehope
中文

Effect preview

Press the "click to preview" button on the right to preview the current page, and click the link to preview in full screen.

https://codepen.io/comehope/pen/QWvVBJq

Source code download

Please download all the source code of the daily front-end combat series from github:

https://github.com/comehope/front-end-daily-challenges

Code interpretation

Function and concept

The origin of this project is that I saw a webpage introduced small cubes with color and put together patterns. It happened that I also had some of these small cubes at home, so I also used it to paint and spell out various patterns. In the process of playing, I came up with the idea of making a designer.

The designer includes 4 functions:

  1. Custom pattern: on the upper left side of the page;
  2. Preview the tiling effect of the pattern: on the right side of the page;
  3. Provide 3 kinds of grid sizes for preview: in the lower right side of the page;
  4. Provide 12 kinds of preset patterns: in the lower left side of the page.

Some business concepts will be mentioned later, they are also variable names in the program:

  • Floor tiles: tile. The floor tile in the upper left corner is called the sample floor tile.
  • A quarter of the floor tiles: block. A floor tile is composed of 4 blocks, and the pattern of each block is a small square with triangles inside.
  • Grid floor: floor. The floor is covered with floor tiles, and there are 3 types of floor sizes: 2x2, 4x4, and 8x8.
  • Preset pattern: pattern. Each preset pattern is a floor tile.

Next, we will implement the four functions of the designer in turn.

The first function: custom patterns

Define the dom structure as follows:

<main>
    <div class="sample">
        <div class="tile">
            <div class="block"></div>
        </div>
    </div>
</main>

All the elements of the program are included in the <main> element, and the <main> element will be enriched with the expansion of functions. .sample sub-element that represents the "sample area", and it also contains a .tile element that represents a floor tile. .tile element should contain four .block elements. Don't worry, first use one .block experiment.

Use CSS pseudo-elements to draw a triangle .block

.tile .block {
    width: 10em;
    height: 10em;
    border: 1px solid grey;
    box-sizing: border-box;
    color: dimgray;
    position: relative;
}

.tile .block::before {
    content: '';
    position: absolute;
    border-width: calc(5em - 1px);
    border-style: solid;
    border-color: transparent;
    border-left-color: currentColor;
}

The effect is as follows:

This triangle occupies a .blcok , which is half of the triangle we need.

Next, draw another triangle. In order to distinguish it from the previous triangle, fill it with black:

.tile .block::before {
    border-top-color: black;
}

The effect is as follows:

Two small triangles joined together to form a large triangle.

Why not just draw a big triangle? Because the CSS method of drawing a right-angled triangle is to use a side of the square as the hypotenuse, and then specify the height of the triangle. If you draw a large triangle directly, you must first construct a large square. The side length of the large square must be a small square. The root of the side is twice as long. Drawing a large triangle is more complicated than drawing two small triangles.

The height of each small triangle should theoretically be half of the side length 5em , here 5em-1px is taken, because the box-sizing: border-box attribute causes the border to occupy 1px into the container.

Refactor it at will, the last three lines of code related to the border color of the ::before

.tile .block::before {
    /*border-color: transparent;
    border-left-color: currentColor;
    border-top-color: black;*/
    border-color: currentColor transparent transparent currentColor;
}

The above code does not specify a clear color value border-color currentColor , so that the main element can control the color, which is convenient for subsequent modification of the color of the floor tiles.

The effect after reconstruction is as follows:

After the above experiment, we have successfully drawn a triangle .block .tile the sub-elements of .block to 4 0610b5b495a844:

<main>
    <div class="sample">
        <div class="tile">
            <div class="block"></div>
            <div class="block"></div>
            <div class="block"></div>
            <div class="block"></div>
        </div>
    </div>
</main>

.block 4 0610b5b495a86c into a grid shape:

.tile {
    width: 20em;
    display: grid;
    grid-template-columns: repeat(2, 1fr);
}

.sample > .tile .block {
    cursor: pointer;
}

The effect is as follows:

At this point, the layout of a floor tile has been completed. Next, we must solve the problem of how to control each .block . First define 4 CSS variables:

:root {
    --block-angle-1: 0;
    --block-angle-2: 0;
    --block-angle-3: 0;
    --block-angle-4: 0;
}

These variables are used to indicate .block . 0 means the vertex is on the upper left, 90 means the vertex is on the upper right, 180 means the vertex is on the lower right, and 270 means the vertex is on the lower left.

Assign these 4 variables to 4 .block and apply them to the transform: rotate() attribute. 0/90/180/270 refers to the rotation angle of the .block

.tile .block:nth-child(1) {transform: rotate(calc(var(--block-angle-1) * 1deg));}
.tile .block:nth-child(2) {transform: rotate(calc(var(--block-angle-2) * 1deg));}
.tile .block:nth-child(3) {transform: rotate(calc(var(--block-angle-3) * 1deg));}
.tile .block:nth-child(4) {transform: rotate(calc(var(--block-angle-4) * 1deg));}

Now, try to modify the values of these 4 CSS variables, and the floor tile pattern will be adjusted accordingly.

Next, write the js code to achieve the effect of adjusting the floor tile pattern by clicking.

First define a dom variable to refer to the dom element, dom.root refers to the CSS :root element, dom.sampleTile is the sample floor tile we just created:

const $ = (selector) => document.querySelector(selector)
let dom = {
    root: document.documentElement,
    sampleTile: $('.sample > .tile'),
}

After the page is loaded, call an initialization function init() complete the binding of events:

window.onload = init()

function init() {
    initEvent()
}

initEvent() function will traverse each .block sample floor tile, and make it execute the rotateBlcok() function when it is clicked. The parameters passed into the function are 1, 2, 3, 4, .block are the serial numbers of the 4 0610b5b495a971 elements. rotateBlock() function reads the .block , gets its rotation angle, and then adds 90, which means to .block by 90 degrees:

function initEvent() {
    Array.from(dom.sampleTile.children).forEach((block, i) => {
        block.addEventListener('click', () => {
            rotateBlock(i + 1)
        })
    })
}

function getCssVariableName(sequenceNumberOfBlcok) {
    return `--block-angle-${sequenceNumberOfBlcok}`
}

function rotateBlock(num) {
    let angle = +dom.root.style.getPropertyValue(getCssVariableName(num)) + 90
    setBlockAngle(num, angle)
}

function setBlockAngle(num, angle) {
    dom.root.style.setProperty(getCssVariableName(num), angle)
}

The other two functions getCssVariableName() and setBlockAngle() do not need to be explained, just look at the names and you will know what they mean. This kind of short, fine-grained function with only one sentence can make the code more semantic and make reading the code smoother.

Try it now, every time you click on any .block , it will rotate 90 degrees.

This is a designed floor tile pattern:

In order to enhance the dynamic, add a transition animation .block

.tile .block {
    transition: 0.2s;
}

The second function: Tile

Next, implement the second function, laying tiles on the floor.

First expand dom, <main> element representing the finished product to the .production element, which contains a sub-element .floor representing the floor:

<main>
    <div class="sample">
        <div class="tile">
            <div class="block"></div>
            <div class="block"></div>
            <div class="block"></div>
            <div class="block"></div>
        </div>
    </div>
    <div class="production">
        <div class="floor"></div>
    </div>
</main>

.floor should contain multiple floor tile elements, which will be automatically created by the program.

Expand the dom variable and add a reference .floor

let dom = {
    root: document.documentElement,
    sampleTile: $('.sample > .tile'),
    floor: $('.production .floor'),
}

init() us expand the initialization function 0610b5b495aa8b and call initFloor() :

function init() {
    initEvent()
    initFloor()
}

initFloor() function then calls the paveTiles() function. The passed-in parameter represents the number of floor tiles on each side of the floor grid. As an experiment, the number 2 passed in, which means that a 2x2 grid floor is to be filled. paveTiles() function implements the .floor of inserting a number of .tile elements in 0610b5b495aac4. node.cloneNode(true) used to get node element in order to copy all its child elements.

function initFloor() {
    paveTiles(2)
}

function paveTiles(countOfPerSide) {
    let count = Math.pow(countOfPerSide, 2)
    dom.floor.innerHTML = ''
    new Array(count).fill('').forEach(() => {
        dom.floor.append(dom.sampleTile.cloneNode(true))
    })
}

Now run the program and you can see that there are indeed a lot of floor tiles, but they are all arranged vertically together with the sample floor tiles. It doesn't matter, adjust the layout with CSS.

First set the <main> element as a whole to the left and right structure layout:

main {
    display: flex;
    justify-content: space-between;
    width: 65em;
}

.sample {width: 20em;}
.production {width: 40em;}

Then arrange the floor into a grid:

.production .floor {
    --count-of-per-side: 2;
    display: grid;
    grid-template-columns: repeat(var(--count-of-per-side), 1fr);
    font-size: calc(2em / var(--count-of-per-side));
}

In this CSS code, another variable --count-of-per-side is defined, which has the paveTile() function. Both represent the number of floor tiles on each side of the floor grid, and the value remains the same. Currently, it is 2. Pay attention to the font-size attribute in this code, it will be adjusted according to the size of each grid, the denser the grid, the smaller the font, so that different density grids can be displayed in the same size container.

Now try to adjust the pattern of the sample floor tiles on the left, and you can see that all the floor tiles on the right floor also change neatly.

The effect is as follows:

The third function: switch the floor grid

Next, implement the third function to adjust the size of the floor grid.

First expansion dom, in .production increasing element grid-list element, which contains three buttons, which is switched to the floor grid size 2x2,4x4,8x8:

<main>
    <div class="sample">
        <div class="tile">
            <div class="block"></div>
            <div class="block"></div>
            <div class="block"></div>
            <div class="block"></div>
        </div>
    </div>
    <div class="production">
        <div class="floor"></div>
        <div class="grid-list">
            <button>2x2</button>
            <button>4x4</button>
            <button>8x8</button>
        </div>
    </div>
</main>

Adjust the layout of these 3 buttons so that they are evenly arranged under the floor:

.production .grid-list {
    display: flex;
    justify-content: space-around;
    margin-top: 2em;
}

.production .grid-list button {
    font-size: 1.5em;
    width: 6em;
    letter-spacing: 0.4em;
    cursor: pointer;
}

Next, modify the program to make these 3 buttons take effect.

First expand the dom variable, add a reference that represents the switch button area dom.gridList

let dom = {
    root: document.documentElement,
    sampleTile: $('.sample > .tile'),
    floor: $('.production .floor'),
    gridList: $('.production .grid-list'),
}

Then expand the initEvent() function to bind the click event to the button. When the button is clicked, call the paveTiles() function to resurface the floor:

function initEvent() {
    Array.from(dom.sampleTile.children).forEach((block, i) => {
        block.addEventListener('click', () => {
            rotateBlock(i + 1)
        })
    })

    Array.from(dom.gridList.children).forEach(button => {
        button.addEventListener('click', (e) => {
            paveTiles(parseInt(e.target.innerText))
        })
    })
}

Now refresh the page and find that the tiles in the floor do increase after clicking the 3 buttons, but the floor cannot accommodate so many tiles, so we have to arrange them downwards. This is because in the previous CSS code, the --count-of-per-side variable is assigned the value 2, so it needs to paveTiles() function to make the grid density automatically adjust with the number of floor tiles:

function paveTiles(countOfPerSide) {
    let count = Math.pow(countOfPerSide, 2)
    dom.floor.innerHTML = ''
    new Array(count).fill('').forEach(() => {
        dom.floor.append(dom.sampleTile.cloneNode(true))
    })

    dom.floor.style.setProperty('--count-of-per-side', countOfPerSide)
}

At this point, the function of switching the floor grid is completed.

The effect is as follows:

Recall that there is a statement paveTiles(2) initFloor() function written earlier, where a number 2 is hard-coded, and now it should be restructured to read the value of the first button, avoiding the use of magic numbers:

function initFloor() {
    paveTiles(parseInt(dom.gridList.children[0].innerText))
}

The fourth function: preset patterns

Next, the fourth function will be developed to show a number of preset patterns for selection.

First expansion dom, in .sample increase element .pattern-list elements:

<main>
    <div class="sample">
        <div class="tile">
            <div class="block"></div>
            <div class="block"></div>
            <div class="block"></div>
            <div class="block"></div>
        </div>
        <div class="pattern-list"></div>
    </div>
    <div class="production">
        <div class="floor"></div>
        <div class="grid-list">
            <button>2x2</button>
            <button>4x4</button>
            <button>8x8</button>
        </div>
    </div>
</main>

.pattern-list similar to the previous .grid-list . Only one container is defined in dom, and the child elements in it are generated by the program.

First define a set of preset pattern data, each preset pattern is an array containing 4 values, storing the angles of the 4 blocks of the floor tiles:

let patterns = [
    [0, 0, 0, 0],
    [0, 90, 270, 180],
    [180, 270, 90, 0],
    [270, 0, 180, 90],
    [90, 270, 270, 90],
    [180, 270, 0, 90],
    [270, 270, 90, 90],
    [270, 180, 0, 90],
    [0, 270, 90, 180],
    [180, 270, 180, 270],
    [270, 180, 180, 270],
    [180, 90, 90, 180],
]

Expand the dom dom.patternList that represents the list of preset patterns:

let dom = {
    root: document.documentElement,
    sampleTile: $('.sample > .tile'),
    floor: $('.production .floor'),
    gridList: $('.production .grid-list'),
    patternList: $('.sample .pattern-list'),
}

Add a line of statement to the initialization function init() , which is used to call the function initPatternList() that initializes the preset pattern list:

function init() {
    initEvent()
    initFloor()
    initPatternList()
}

initPatternList() function is to fill in the .pattern-list
The concrete realization of the charge element. paveTiles() function 0610b5b495adf6, the sub-elements are also replicated multiple times on the sample floor tiles. When copying, patterns to the newly generated floor tiles:

function initPatternList() {
    patterns.forEach((pattern) => {
        let $newTile = dom.sampleTile.cloneNode(true)
        Array.from($newTile.children).forEach((block, i) => {
            let property = `--block-angle-${i + 1}`
            block.style.setProperty(property, pattern[i])
        })
        dom.patternList.append($newTile)
    })
}

Now refresh the page, you can see that the preset pattern has been displayed on the left side of the page, but it is mixed with the sample floor tiles, so you need to adjust the layout so that the preset patterns are arranged below the sample floor tiles in the form of thumbnails. The method of shrinking the preset pattern is the same as the method of shrinking the floor tiles on the floor, which is achieved by adjusting the properties of font-size

.sample > .tile {
    margin-bottom: 4em;
}

.sample .pattern-list {
    font-size: 0.2em;
    display: grid;
    grid-template-columns: repeat(4, 1fr);
    gap: 5em;
}

.sample .pattern-list .tile {
    cursor: pointer;
}

Then set the color of the first preset pattern lighter, because this pattern is the default undesigned pattern, so it is different from other preset patterns:

.sample .pattern-list .tile:first-child .block {
    color: lightgrey;
}

Now refresh the page again and see the effect as shown below:

Next, add a click effect to the preset pattern.

Expand the initEvent() function to dom.patternList , so that when any one of the preset patterns is clicked, the angle data of the preset pattern is copied to :root element, so that the preset pattern can be applied to the sample floor tiles And on the floor:

function initEvent() {
    Array.from(dom.sampleTile.children).forEach((block, i) => {
        block.addEventListener('click', () => {
            rotateBlock(i + 1)
        })
    })

    Array.from(dom.gridList.children).forEach(button => {
        button.addEventListener('click', (e) => {
            paveTiles(parseInt(e.target.innerText))
        })
    })

    Array.from(dom.patternList.children).forEach((tile, i) => {
        tile.addEventListener('click', () => {
            patterns[i].forEach((angle, j) => setBlockAngle(j + 1, angle))
        })
    })
}

Also adjust init() , first render the element, and then bind the event to the element:

function init() {
    initFloor()
    initPatternList()
    initEvent()
}

Now refresh the page, try to click the preset pattern, the click event has taken effect.

At this point, the development of all functions is complete.

Interface beautification

Finally, beautify the interface.

First add a <h1> element to dom and write the title:

<h1>Tile Pattern Designer</h1>
<main>
    <!-- 略 -->
</main>

Set the overall style of the page, including background color, font size, and center alignment:

body {
    margin: 0 auto;
    height: 100vh;
    display: flex;
    flex-direction: column;
    align-items: center;
    font-size: 0.75em;
    font-family: sans-serif;
    background: linear-gradient(to right bottom, lightcyan, lightblue);
}

h1 {
    font-weight: normal;
    margin: 2em;
    letter-spacing: 0.1em;
}

Add an outer frame to the sample pattern and the floor, emphasizing that they are an independent whole:

.sample > .tile,
.production .floor {
    box-shadow: 
        0 0 0 9px lightcyan,
        0 0 0 10px grey;
}

The effect is as follows:

That's it!

About the author

Zhang Ou, whose pen name is comehope, touched the Internet at the end of the 20th century and was captured by the infinite charm of the Web. Since then, he has been fighting on the front line of Web development.

The column of "Front-end Daily Combat" is my notes on practical project-based learning in recent years. Project-driven learning shows the complete process from inspiration flash to code implementation. It can also be used as a practice exercise and development reference for front-end development.

The book "CSS3 Art" has been published by People's Posts and Telecommunications Publishing House and is printed in full color. With more than 100 vivid and beautiful examples, it systematically analyzes the important grammar related to CSS and visual effects, and contains nearly 10 hours of video demonstration. . Available on JD.com/Tmall/Dangdang.

阅读 812

前端每日实战
?该专栏由《CSS3 艺术》一书的作者亲自维护,已累计分享 170+ 个前端项目从灵感闪现到代码实现的完整过...

💯累计分享170+个项目💯

9.3k 声望
14.4k 粉丝
0 条评论
你知道吗?

💯累计分享170+个项目💯

9.3k 声望
14.4k 粉丝
文章目录
宣传栏