乐趣区

关于前端:前端每日实战第178号作品地砖图案设计器

成果预览

按下右侧的“点击预览”按钮能够在以后页面预览,点击链接能够全屏预览。

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

源代码下载

每日前端实战系列的全副源代码请从 github 下载:

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

代码解读

性能和概念

这个我的项目的起源是我看到一个网页介绍把小立方块涂上色彩拼成图案,正好我家里也有一些这样的小立方块,于是也拿来涂了色拼出了各种花色,在玩儿的过程中我产生了做一个设计器的想法。

设计器包含 4 个性能:

  1. 自定义图案:在页面左侧上部;
  2. 预览图案的平铺成果:在页面右侧;
  3. 提供 3 种供预览的网格尺寸:在页面右侧下部;
  4. 提供 12 种预设的图案:在页面左侧下部。

前面会提到一些业务概念,它们也是程序里的变量名:

  • 地砖:tile。左上角地砖称为样品地砖。
  • 地砖的四分之一:block。一块地砖由 4 个 block 组成,每个 block 的图案是 1 个内含三角形的小正方形。
  • 网格状地板:floor。地板上铺满了地砖,地板尺寸有 3 种:2×2、4×4、8×8。
  • 预设图案:pattern。每个预设图案就是一块地砖。

接下来就顺次实现设计器的 4 个性能。

第 1 个性能:自定义图案

定义 dom 构造如下:

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

程序的所有元素都被蕴含在 <main> 元素里,<main> 元素会随着性能的裁减不断丰富。当初它外面有一个示意“样品区”的 .sample 子元素,其中再蕴含一个示意地砖的 .tile 元素。.tile 元素外面本应蕴含 4 个 .block 元素,不急,先用 1 个 .block 做做试验。

用 CSS 的伪元素在 .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;
}

成果如下图:

这个三角形占据了正方形 .blcok 的四分之一空间,它是咱们须要的三角形的一半。

接下来再画另一个三角形,为了和前一个三角形区别开,把它填充成彩色:

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

成果如下图:

两个小三角形拼成了一个大三角形。

为什么不间接画一个大三角形呢?因为 CSS 画直角三角形的办法,是以正方形的某一条边作为斜边,而后指定三角形的高,如果间接画大三角形,那就要先结构一个大正方形,这个大正方形的边长需是小正方形边长的根号 2 倍,画一个大三角形比画 2 个小三角形还要简单呢。

每个小三角形的高实践上应该是边长的一半 5em,这里取 5em-1px,是因为 box-sizing: border-box 属性导致边框向容器内强占了 1px

顺手重构一下,::before 伪元素的最初三行无关边框色彩的代码能够合并写成一行:

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

以上代码没有在伪元素的 border-color 属性中指定明确色值,应用的是 currentColor,这样就能够由主元素来管制色彩,便于后续批改地砖的色彩。

重构后成果如下图:

通过下面的试验,曾经胜利在一个 .block 里画出了三角形,当初把 .tile 的子元素减少到 4 个 .block

<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>

将 4 个 .block 组成一个田字格形态:

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

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

成果如下图:

至此,一块地砖的布局已实现,接下来,要解决如何管制每个 .block 的问题。先定义 4 个 CSS 变量:

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

这些变量用来示意 4 个 .block 中三角形顶点的地位,0 示意顶点在左上,90 示意顶点在右上,180 示意顶点在右下,270 示意顶点在左下。

把这 4 个变量调配给 4 个 .block,利用到 transform: rotate() 属性中,0/90/180/270 指的就是 .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));}

当初,试一试批改这 4 个 CSS 变量值,地砖图案会跟着被调整。

接下来写 js 代码,实现通过点击来调整地砖图案的成果。

先定义一个 dom 变量,用于援用 dom 元素,dom.root 是指 CSS 的 :root 元素,dom.sampleTile 就是咱们刚刚创立的样品地砖:

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

在页面加载实现后调用一个初始化函数 init(),在其中实现对事件的绑定:

window.onload = init()

function init() {initEvent()
}

initEvent() 函数将遍历样品地砖的每一个 .block,令其在被点击时执行 rotateBlcok() 函数,传入该函数的参数别离是 1、2、3、4,是 4 个 .block 元素的序号。rotateBlock() 函数读取该 .block 对应的 CSS 变量,失去它的旋转角度,而后加上 90,意即让这个 .block 旋转 90 度:

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)
}

另外 2 个函数 getCssVariableName()setBlockAngle() 不必解释,看名字就晓得是什么意思了。这种简短的、甚至只有一条语句的细粒度函数,能让代码更加语义化,让浏览代码更晦涩。

当初试一试,每点击一下任意一个 .block,它就会旋转 90 度。

这是一个通过设计的地砖图案:

为了增强动感,再给 .block 加一个过渡动画:

.tile .block {transition: 0.2s;}

第 2 个性能:平铺

接下来实现第 2 个性能,在地板上平铺地砖。

先裁减 dom,为 <main> 元素减少一个示意成品的 .production 元素,其中蕴含一个示意地板的子元素 .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 外面应该蕴含多个地砖元素,这些元素将通过程序主动创立。

裁减一下 dom 变量,减少对 .floor 的援用:

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

再裁减一下初始化函数 init(),在其中调用 initFloor()

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

initFloor() 函数再调用 paveTiles() 函数,传入的参数示意地板网格每边的地砖数量,作为试验,传入数字 2,示意要填充一个 2×2 网格的地板。paveTiles() 函数实现在 .floor 中插入若干 .tile 元素的操作。node.cloneNode(true) 用于失去 node 元素的深拷贝,以便复制它的所有子元素。

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))
    })
}

当初运行一下程序,能看到的确多了很多地砖,但它们都和样品地砖竖向排在一起。没关系,用 CSS 调整一下布局。

先把 <main> 元素整体设置为左右构造布局:

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

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

而后把地板排列成网格状:

.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));
}

在这段 CSS 代码中,又定义了一个变量 --count-of-per-side,它和 paveTile() 函数的参数是同样的含意,都示意地板网格每边的地砖数量,而且值也保持一致,目前都是 2。留神这段代码中的 font-size 属性,它会依据每网格大小来调整,网格越密,字体就越小,以便在同样大小的容器内能够显示不同密度的网格。

当初试试调整左侧的样品地砖的图案,能看到右侧地板的所有地砖也跟着整齐划一地跟着变动。

成果如下图:

第 3 个性能:切换地板网格

接下来实现第 3 个性能,调整地板网格的尺寸。

先裁减 dom,在 .production 元素中减少 grid-list 元素,其中蕴含 3 个按钮,别离用于把地板网格切换为 2×2、4×4、8×8 的尺寸:

<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>

调整一下这 3 个按钮的布局,让它们平均地排列在地板下方:

.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;
}

接下来批改程序,让这 3 个按钮失效。

先裁减一下 dom 变量,减少一个示意切换按钮区 dom.gridList 的援用:

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

再裁减 initEvent() 函数,为按钮绑定点击事件,当按钮被点击时,调用 paveTiles() 函数重铺地板:

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))
        })
    })
}

当初刷新一下页面,发现点击 3 个按钮之后地板中的地砖的确减少了,然而地板包容不下那么多地砖,只好向下排列。这是因为在后面的 CSS 代码中,--count-of-per-side 变量被赋值为 2,所以须要在 paveTiles() 函数中更新它,使网格密度随着地砖的数量主动调整:

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)
}

至此,切换地板网格的性能就实现了。

成果如下图:

回忆一下之前写的 initFloor() 函数中有一条语句 paveTiles(2),这里硬编码了一个数字 2,当初应该把它重形成读取第 1 个按钮的数值,防止应用魔法数字:

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

第 4 个性能:预设图案

接下来开发第 4 个性能,展现若干预设图案供选择。

先裁减 dom,在 .sample 元素中减少 .pattern-list 元素:

<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 和后面的 .grid-list 相似,dom 中只定义了一个容器,其中的子元素都要由程序生成。

先定义一组预设图案数据,每个预设图案是一个含有 4 个数值的数组,存储着地砖的 4 个 block 的角度:

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],
]

裁减 dom 变量,减少一个示意预设图案列表的援用 dom.patternList

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

初始化函数 init() 中减少一行语句,用于调用初始化预设图案列表的函数 initPatternList()

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

initPatternList() 函数是为 .pattern-list 元素填
充子元素的具体实现。和 paveTiles() 函数相似,子元素也是对样品地砖进行了屡次复制。在复制时,还要把变量 patterns 的数据写到新生成的地砖上:

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)
    })
}

当初刷新一下页面,能够看到预设图案曾经显示到页面左侧了,然而和样品地砖混在一起,所以要调整一下布局,让预设图案以缩略图的模式排列在样品地砖的下方。使预设图案放大的办法和使地板上地砖放大的办法一样,都是通过调整 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;}

再把第 1 个预设图案的色彩设置得浅一些,因为这个图案是默认的未经设计的图案,所以让它和其余预设图案有所区别:

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

当初再刷新一下页面,看到成果如下图:

接下来为预设图案减少点击成果。

裁减 initEvent() 函数,为 dom.patternList 的子元素绑定点击事件,令点击任意一个预设图案时,把预设图案的角度数据复制到 :root 元素的同名变量中,这样就能够把预设图案利用到样品地砖和地板上:

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))
        })
    })
}

还要调整一下初始化函数 init() 中语句的执行程序,先渲染元素、再为元素绑定事件:

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

当初刷新一下页面,试试点击预设图案,点击事件曾经失效了。

至此,全副性能开发实现。

界面丑化

最初,丑化一下界面。

先在 dom 中减少一个 <h1> 元素,写上题目:

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

设置页面整体款式,包含背景色、字体字号、居中对齐:

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;
}

为样品图案和地板减少一个外边框,强调它们是独立的整体:

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

成果如下图:

功败垂成!

对于作者

张偶,网络笔名 comehope,20 世纪末触网,被 Web 的无穷魅力所俘获,自此始终战斗在 Web 开发第一线。

《前端每日实战》专栏是我近年来实际我的项目式学习的笔记,以我的项目驱动学习,展示从灵感闪现到代码实现的残缺过程,亦可作为前端开发的练手习题和开发参考。

拙作《CSS3 艺术》一书已由人民邮电出版社出版,全彩印刷,用 100 多个活泼好看的实例,系统地分析了 CSS 与视觉效果相干的重要语法,并含有近 10 小时的视频演示解说。京东 / 天猫 / 当当有售。

退出移动版