首先写最基础的button组件
<script setup>
const props = defineProps({})
const emit = defineEmits(["click"])
const handleClick = (evt) => { emit("click", evt) }
</script>
<template>
<button
class="neumorphism"
@click="handleClick"
>
<span v-if="$slots.default"><slot></slot></span>
</button>
</template>
<script>
export default {
name: 'sanorin-button',
}
</script>
<style>
.neumorphism {
width: 200px;
height: 75px;
border: none;
border-radius: 20px;
background-color: #e6e6e6;
box-shadow: 5px 5px 10px #bbbbbb, -5px -5px 10px #ffffff;
}
.neumorphism:active {
box-shadow: inset 5px 5px 10px #bbbbbb, inset -5px -5px 10px #ffffff;
}
.neumorphism:focus {
outline: none;
}
.neumorphism span {
display: block;
user-select: none;
color: #a6a6a6;
font-size: 24px;
text-align: center;
line-height: 75px;
transition: 0.1s;
}
.neumorphism:active span {
transform: scale(0.95);
}
</style>
第二步,需要颜色自定义,这里可以用prop传入,我这里采用的是跟随全局主题颜色,全局主题颜色是用户在网页上根据调色板选出的,js部分用provide/inject取出,css部分用–main-color取出。
于是这里就需要根据主题色,生产出.neumorphism的样式。相关代码如下:
// hexColor.js
export const hexColor = (hex, lum) => {
// validate hex string
hex = String(hex).replace(/[^0-9a-f]/gi, '')
if (hex.length < 6) {
hex = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2]
}
lum = lum || 0
// convert to decimal and change luminosity
let rgb = '#',
c,
i
for (i = 0; i < 3; i++) {
c = parseInt(hex.substr(i * 2, 2), 16)
c = Math.round(Math.min(Math.max(0, c + c * lum), 255)).toString(16)
rgb += ('00' + c).substr(c.length)
}
return rgb
}
export const getContrast = (hex) => {
const r = parseInt(hex.substr(1, 2), 16),
g = parseInt(hex.substr(3, 2), 16),
b = parseInt(hex.substr(5, 2), 16),
yiq = (r * 299 + g * 587 + b * 114) / 1000
return yiq >= 128 ? '#001f3f' : '#F6F5F7'
}
<script setup>
import { inject, computed } from 'vue'
import { hexColor } from '@/sanorin/utils/hexColor'
const emit = defineEmits(["click"])
const handleClick = (evt) => { emit("click", evt) }
const mainColor = inject('mainColor')
let leftColor = computed(() => hexColor(mainColor.value,-0.15))
let rightColor = computed(() => hexColor(mainColor.value,0.15))
</script>
<template>
<button
class="neumorphism"
:style="{ '--left-color':leftColor,'--right-color':rightColor }"
@click="handleClick"
>
<span v-if="$slots.default"><slot></slot></span>
</button>
</template>
<script>
export default {
name: 'sanorin-button',
}
</script>
<style>
.neumorphism {
width: 200px;
height: 75px;
border: none;
border-radius: 20px;
background-color: var(--main-color);
box-shadow: 10px 10px 30px var(--left-color),-10px -10px 30px var(--right-color);
}
.neumorphism:active {
box-shadow: inset 10px 10px 30px var(--left-color), inset -10px -10px 30px var(--right-color);
}
.neumorphism:focus {
outline: none;
}
.neumorphism span {
display: block;
user-select: none;
color: var(--text-color);
font-size: 24px;
text-align: center;
line-height: 75px;
transition: 0.1s;
}
.neumorphism:active span {
transform: scale(0.95);
}
</style>
------------------------------------------------分割线-------------------------------------------
这样雏形就有了,接下来处理props,首先加一个type类型,有四种类型,其中第四种我写到了按下去的样式,所以也就是前三种,我将其命名为:plain,down,up。
<script setup>
import { inject, computed } from 'vue'
import { hexColor } from '@/sanorin/utils/hexColor'
const props = defineProps({
type: {
type: String,
default: 'plain',
}
})
const emit = defineEmits(["click"])
const handleClick = (evt) => { emit("click", evt) }
const mainColor = inject('mainColor')
let lightColor = computed(() => [hexColor(mainColor.value,-0.15),hexColor(mainColor.value,0.15)])
let linearColor = computed(() => ((e) => ({
'down': () => [hexColor(mainColor.value,-0.1),hexColor(mainColor.value,0.07)],
'up': () => [hexColor(mainColor.value,0.07),hexColor(mainColor.value,-0.1)],
})[e] || (() => [mainColor.value,mainColor.value]))(props.type)())
</script>
<template>
<button
class="neumorphism"
:style="{ '--left-color':lightColor[0],'--right-color':lightColor[1], '--linear-left-color':linearColor[0], '--linear-right-color':linearColor[1] }"
@click="handleClick"
>
{{linearColor}}
<span v-if="$slots.default"><slot></slot></span>
</button>
</template>
<script>
export default {
name: 'sanorin-button',
}
</script>
<style>
.neumorphism {
width: 200px;
height: 75px;
border: none;
border-radius: 20px;
background: linear-gradient(145deg, var(--linear-left-color), var(--linear-right-color));
box-shadow: 10px 10px 30px var(--left-color),-10px -10px 30px var(--right-color);
}
.neumorphism:active {
box-shadow: inset 10px 10px 30px var(--left-color), inset -10px -10px 30px var(--right-color);
}
.neumorphism:focus {
outline: none;
}
.neumorphism span {
display: block;
user-select: none;
color: var(--text-color);
font-size: 24px;
text-align: center;
line-height: 75px;
transition: 0.1s;
}
.neumorphism:active span {
transform: scale(0.95);
}
</style>
1接下来控制光照的角度,如果允许入参是360°的话,还要用公式来计算box-shadow的值,太麻烦,懒得写了,因此只支持4个角好了。即光从左上角右上角左下角右下角照过来。我将其命名为:left-up,right-up,left-down,right-down。
2再控制物体的高度,我将其命名为:distance
3在控制的柔和程度,我将其命名为:blur
<script setup>
import { ref, inject, computed } from 'vue'
import { hexColor } from '@/sanorin/utils/hexColor'
const intensity = 0.15 // 这个参数影响颜色的变化率,模拟物体的反光程度,暂时没暴露给外部,因为仅在0.1到0.3之间模拟的比较真实,如果要暴露出去,可以设定0.1为0%,0.3为100%。
console.log(-intensity/3*2)
console.log(intensity/2)
const props = defineProps({
type: { // 按钮凹凸参数
type: String,
default: 'plain',
},
light: { // 光射来的方向参数
type: String,
default: 'left-up',
},
distance: { // 物体的高度参数
type: Number,
default: 10,
},
blur: { // 光的柔和程度参数
type: Number,
default: 30,
}
})
const emit = defineEmits(["click"])
const handleClick = (evt) => { emit("click", evt) }
let mainColor = inject('mainColor')
let linearAngle = computed(() => ((e) => ({ // 控制linear的角度,模拟光射来的方向
'right-up': () => '225deg',
'left-down': () => '45deg',
'rihgt-down': () => '315deg',
})[e] || (() => '145deg'))(props.light)())
let linearColor = computed(() => ((e) => ({ // 控制linear的颜色,模拟物体的顶板凹凸状态
'up': () => [hexColor(mainColor.value,-intensity/3*2),hexColor(mainColor.value,intensity/2)],
'down': () => [hexColor(mainColor.value,intensity/2),hexColor(mainColor.value,-intensity/3*2)],
})[e] || (() => [mainColor.value,mainColor.value]))(props.type)())
let lightColor = computed(() => [hexColor(mainColor.value,-intensity),hexColor(mainColor.value,intensity)]) // 控制shadow的颜色,模拟物体的阴影深浅
let shadowSize = computed(() => new Array(4).fill(props.distance)) // 控制shadow的大小,模拟物体的高度
let lightBeam = computed(() => ((e) => ({ // 控制shadow的正负,模拟不同方向射来的光
'right-up': () => [-1, 1, 1, -1],
'left-down': () => [1, -1, -1, 1],
'rihgt-down': () => [-1, -1, 1, 1],
})[e] || (() => [1, 1, -1, -1]))(props.light)())
let boxShadow = computed(() => new Array(4).fill().reduce((t,v,i) => { // 结合shadow的大小和正负,生成shadow
t.push(shadowSize.value[i] * lightBeam.value[i] + 'px')
return t
},[]))
let boxBlur = computed(() => props.blur + 'px') // 控制shadow的blur,模拟不同柔度的光
</script>
<template>
<button
class="neumorphism"
:style="{
'--left-color':lightColor[0], '--right-color':lightColor[1], '--linear-left-color':linearColor[0], '--linear-right-color':linearColor[1], '--linear-angle':linearAngle,
'--shadow-ah':boxShadow[0], '--shadow-av':boxShadow[1], '--shadow-bh':boxShadow[2], '--shadow-bv':boxShadow[3], '--shadow-blur':boxBlur
}"
@click="handleClick"
>
{{linearColor}}
<span v-if="$slots.default"><slot></slot></span>
</button>
</template>
<script>
export default {
name: 'sanorin-button',
}
</script>
<style>
.neumorphism {
width: 200px;
height: 75px;
border: none;
border-radius: 20px;
background: linear-gradient(var(--linear-angle), var(--linear-left-color), var(--linear-right-color));
box-shadow: var(--shadow-ah) var(--shadow-av) 30px var(--left-color),var(--shadow-bh) var(--shadow-bv) var(--shadow-blur) var(--right-color);
}
.neumorphism:active {
box-shadow: inset var(--shadow-ah) var(--shadow-av) var(--shadow-blur) var(--left-color), inset var(--shadow-bh) var(--shadow-bv) var(--shadow-blur) var(--right-color);
}
.neumorphism:focus {
outline: none;
}
.neumorphism span {
display: block;
user-select: none;
color: var(--text-color);
font-size: 24px;
text-align: center;
line-height: 75px;
transition: 0.1s;
}
.neumorphism:active span {
transform: scale(0.95);
}
.neumorphism + .neumorphism{
margin-left: 30px;
}
</style>
接下来写hover时候的变化,这里使用animate,鼠标从左边进入按钮就把按钮往右边挤,鼠标从下方进去就把按钮往上挤,效果图如下:
详细计算思路看下一篇文章 链接待补充
接下来增加圆角按钮样式,很简单不展示代码了。
接下来增加icon支持
<script setup>
import { ref, reactive, inject, computed, onMounted, onUnmounted } from 'vue'
import { hexColor } from '@/sanorin/utils/hexColor'
const intensity = 0.15 // 这个参数影响颜色的变化率,模拟物体的反光程度,暂时没暴露给外部,因为仅在0.1到0.3之间模拟的比较真实,如果要暴露出去,可以设定0.1为0%,0.3为100%。
const props = defineProps({
type: { // 按钮凹凸参数
type: String,
default: 'plain',
},
light: { // 光射来的方向参数
type: String,
default: 'left-up',
},
distance: { // 物体的高度参数
type: Number,
default: 10,
},
blur: { // 光的柔和程度参数
type: Number,
default: 30,
},
animate: { // hover时候移动的距离
type: Number,
default: 10,
},
preIcon: {
type: String,
default: null,
},
postIcon: {
type: String,
default: null,
}
})
const emit = defineEmits(["click"])
const handleClick = (evt) => { emit("click", evt) }
let mainColor = inject('mainColor')
let linearAngle = computed(() => ((e) => ({ // 控制linear的角度,模拟光射来的方向
'right-up': () => '225deg',
'left-down': () => '45deg',
'rihgt-down': () => '315deg',
})[e] || (() => '145deg'))(props.light)())
let linearColor = computed(() => ((e) => ({ // 控制linear的颜色,模拟物体的顶板凹凸状态
'up': () => [hexColor(mainColor.value,-intensity/3*2),hexColor(mainColor.value,intensity/2)],
'down': () => [hexColor(mainColor.value,intensity/2),hexColor(mainColor.value,-intensity/3*2)],
})[e] || (() => [mainColor.value,mainColor.value]))(props.type)())
let lightColor = computed(() => [hexColor(mainColor.value,-intensity),hexColor(mainColor.value,intensity)]) // 控制shadow的颜色,模拟物体的阴影深浅
let shadowSize = computed(() => new Array(4).fill(props.distance)) // 控制shadow的大小,模拟物体的高度
let lightBeam = computed(() => ((e) => ({ // 控制shadow的正负,模拟不同方向射来的光
'right-up': () => [-1, 1, 1, -1],
'left-down': () => [1, -1, -1, 1],
'rihgt-down': () => [-1, -1, 1, 1],
})[e] || (() => [1, 1, -1, -1]))(props.light)())
let boxShadow = computed(() => new Array(4).fill().reduce((t,v,i) => { // 结合shadow的大小和正负,生成shadow
t.push(shadowSize.value[i] * lightBeam.value[i] + 'px')
return t
},[]))
let boxBlur = computed(() => props.blur + 'px') // 控制shadow的blur,模拟不同柔度的光
// 定义按钮位置
const btn = ref() // 与html中ref=""对应,定位dom元素
const btnState = reactive({x:0,y:0})
// 定义[鼠标距离按钮的]相对位置
const mouseBtnRange = reactive({x:0,y:0})
const update = e=>{
let x = e.pageX - btnState.x // [鼠标距离按钮的相对位置的]两条直角边的边长, e.pageX是鼠标绝对位置
let y = e.pageY - btnState.y
mouseBtnRange.x = -(props.animate * x / Math.sqrt(x**2 + y**2)).toFixed(2) + 'px' // 当斜边为animate时,等比缩小,求缩小后的[鼠标距离按钮的相对位置的]两条直角边边长。
mouseBtnRange.y = -(props.animate * y / Math.sqrt(x**2 + y**2)).toFixed(2) + 'px'
}
onMounted(() => {
// 计算按钮位置
let btnDom = btn.value.getBoundingClientRect()
btnState.x = btnDom.x + btnDom.width/2
btnState.y = btnDom.y + btnDom.height/2
// 监控鼠标位置
window.addEventListener('mousemove',update)
})
onUnmounted(()=>{
// 卸载监控鼠标位置
window.removeEventListener('mousemove',update);
})
</script>
<template>
<button
ref="btn"
class="neumorphism"
:style="{
'--left-color':lightColor[0], '--right-color':lightColor[1], '--linear-left-color':linearColor[0], '--linear-right-color':linearColor[1], '--linear-angle':linearAngle,
'--shadow-ah':boxShadow[0], '--shadow-av':boxShadow[1], '--shadow-bh':boxShadow[2], '--shadow-bv':boxShadow[3], '--shadow-blur':boxBlur,
'--hover-sin':mouseBtnRange.x, '--hover-cos':mouseBtnRange.y,
}"
@click="handleClick"
>
<i :class="[preIcon, 'iconfont']" v-if="preIcon"></i>
<span v-if="$slots.default"><slot></slot></span>
<i :class="[postIcon, 'iconfont']" v-if="postIcon"></i>
</button>
</template>
<script>
export default {
name: 'sanorin-button',
}
</script>
<style>
.neumorphism.round{
border-radius: 20px;
}
.neumorphism {
min-width: 40px;
height: 40px;
border: none;
border-radius: 10px;
background: linear-gradient(var(--linear-angle), var(--linear-left-color), var(--linear-right-color));
box-shadow: var(--shadow-ah) var(--shadow-av) 30px var(--left-color),var(--shadow-bh) var(--shadow-bv) var(--shadow-blur) var(--right-color);
}
.neumorphism:active {
box-shadow: inset var(--shadow-ah) var(--shadow-av) var(--shadow-blur) var(--left-color), inset var(--shadow-bh) var(--shadow-bv) var(--shadow-blur) var(--right-color);
}
.neumorphism:hover{
transform: translate(var(--hover-sin),var(--hover-cos));
transition: all .2s ease;
}
.neumorphism:focus {
outline: none;
}
.neumorphism span {
display: block;
user-select: none;
color: var(--text-color);
font-size: 14px;
text-align: center;
transition: 0.1s;
}
.neumorphism:active span {
transform: scale(0.95);
}
.neumorphism + .neumorphism{
margin-left: 30px;
}
</style>
最后就是这样,还有一个loading功能和disable功能没写,以后再补充。
现在提取一下公共代码出来,因为假设我接下来写个table组件,其中有一部分代码是可以复用的,总不能复制过去吧。
首先判断一下是阴影那部分能复用,怎么提取出来呢?
假设我提取到总页面,写一个方法,在总页面时候加载入,生成对应的css,这样也可以,但是整个页面就必须是统一的阴影样式,不太合理。
假设我提取到一个基底组件,然后每次写新组件的时候都用基底组件包裹,还要在自定义组件中接prop,再传入基底,要引入组件,还要在自定义组件中多写代码,没解决问题。
--------------------------接下来都是优化部分-------------------------
只能写一个mixin,vue3中mixin建议用函数式编程代替,我这边修改一下代码,使得更合适函数式编程,修改完后再提取出。
提取后的代码展示:
// neumorphism.js
import { defineProps, inject, computed } from 'vue'
import { hexColor } from '@/sanorin/utils/hexColor'
export const useProp = {
type: { // 按钮凹凸参数
type: String,
default: 'plain',
},
light: { // 光射来的方向参数
type: String,
default: 'left-up',
},
distance: { // 物体的高度参数
type: Number,
default: 10,
},
blur: { // 光的柔和程度参数
type: Number,
default: 30,
}
}
export function useNeumorphism(props) {
const intensity = 0.15 // 这个参数影响颜色的变化率,模拟物体的反光程度,暂时没暴露给外部,因为仅在0.1到0.3之间模拟的比较真实,如果要暴露出去,可以设定0.1为0%,0.3为100%。
console.log(props)
let mainColor = inject('mainColor')
let linearAngle = computed(() => ((e) => ({ // 控制linear的角度,模拟光射来的方向
'right-up': () => '225deg',
'left-down': () => '45deg',
'rihgt-down': () => '315deg',
})[e] || (() => '145deg'))(props.light)())
let linearColor = computed(() => ((e) => ({ // 控制linear的颜色,模拟物体的顶板凹凸状态
'up': () => [hexColor(mainColor.value,-intensity/3*2),hexColor(mainColor.value,intensity/2)],
'down': () => [hexColor(mainColor.value,intensity/2),hexColor(mainColor.value,-intensity/3*2)],
})[e] || (() => [mainColor.value,mainColor.value]))(props.type)())
let lightColor = computed(() => [hexColor(mainColor.value,-intensity),hexColor(mainColor.value,intensity)]) // 控制shadow的颜色,模拟物体的阴影深浅
let shadowSize = computed(() => new Array(4).fill(props.distance)) // 控制shadow的大小,模拟物体的高度
let lightBeam = computed(() => ((e) => ({ // 控制shadow的正负,模拟不同方向射来的光
'right-up': () => [-1, 1, 1, -1],
'left-down': () => [1, -1, -1, 1],
'rihgt-down': () => [-1, -1, 1, 1],
})[e] || (() => [1, 1, -1, -1]))(props.light)())
let boxShadow = computed(() => new Array(4).fill().reduce((t,v,i) => { // 结合shadow的大小和正负,生成shadow
t.push(shadowSize.value[i] * lightBeam.value[i] + 'px')
return t
},[]))
let boxBlur = computed(() => props.blur + 'px') // 控制shadow的blur,模拟不同柔度的光
let baseStyleObject = computed(() => ({
'--left-color':lightColor.value[0], '--right-color':lightColor.value[1], '--linear-left-color':linearColor.value[0], '--linear-right-color':linearColor.value[1], '--linear-angle':linearAngle.value,
'--shadow-ah':boxShadow.value[0], '--shadow-av':boxShadow.value[1], '--shadow-bh':boxShadow.value[2], '--shadow-bv':boxShadow.value[3], '--shadow-blur':boxBlur.value,
}))
return { baseStyleObject }
}
// button.vue
<script setup>
import { ref, reactive, inject, computed, onMounted, onUnmounted } from 'vue'
import { useProp, useNeumorphism } from '../mixin/neumorphism'
const props = defineProps({
...useProp,
...{
animate: { // hover时候移动的距离
type: Number,
default: 10,
},
preIcon: {
type: String,
default: null,
},
postIcon: {
type: String,
default: null,
}
}
})
const { baseStyleObject } = useNeumorphism(props)
const emit = defineEmits(["click"])
const handleClick = (evt) => { emit("click", evt) }
// 定义按钮位置
const btn = ref() // 与html中ref=""对应,定位dom元素
const btnState = reactive({x:0,y:0})
// 定义[鼠标距离按钮的]相对位置
const mouseBtnRange = reactive({x:0,y:0})
const update = e=>{
let x = e.pageX - btnState.x // [鼠标距离按钮的相对位置的]两条直角边的边长, e.pageX是鼠标绝对位置
let y = e.pageY - btnState.y
mouseBtnRange.x = -(props.animate * x / Math.sqrt(x**2 + y**2)).toFixed(2) + 'px' // 当斜边为animate时,等比缩小,求缩小后的[鼠标距离按钮的相对位置的]两条直角边边长。
mouseBtnRange.y = -(props.animate * y / Math.sqrt(x**2 + y**2)).toFixed(2) + 'px'
}
onMounted(() => {
// 计算按钮位置
let btnDom = btn.value.getBoundingClientRect()
btnState.x = btnDom.x + btnDom.width/2
btnState.y = btnDom.y + btnDom.height/2
// 监控鼠标位置
window.addEventListener('mousemove',update)
})
onUnmounted(()=>{
// 卸载监控鼠标位置
window.removeEventListener('mousemove',update);
})
let styleObject = computed(() => ({
'--hover-sin':mouseBtnRange.x, '--hover-cos':mouseBtnRange.y,
}))
</script>
<template>
<button
ref="btn"
class="neumorphism"
:style="{...baseStyleObject,...styleObject}"
@click="handleClick"
>
<i :class="[preIcon, 'iconfont']" v-if="preIcon"></i>
<span v-if="$slots.default"><slot></slot></span>
<i :class="[postIcon, 'iconfont']" v-if="postIcon"></i>
</button>
</template>
<script>
export default {
name: 'sanorin-button',
}
</script>
<style>
.neumorphism.round{
border-radius: 20px;
}
.neumorphism {
min-width: 40px;
height: 40px;
border: none;
border-radius: 10px;
background: linear-gradient(var(--linear-angle), var(--linear-left-color), var(--linear-right-color));
box-shadow: var(--shadow-ah) var(--shadow-av) 30px var(--left-color),var(--shadow-bh) var(--shadow-bv) var(--shadow-blur) var(--right-color);
}
.neumorphism:active {
box-shadow: inset var(--shadow-ah) var(--shadow-av) var(--shadow-blur) var(--left-color), inset var(--shadow-bh) var(--shadow-bv) var(--shadow-blur) var(--right-color);
}
.neumorphism:hover{
transform: translate(var(--hover-sin),var(--hover-cos));
transition: all .2s ease;
}
.neumorphism:focus {
outline: none;
}
.neumorphism span {
display: block;
user-select: none;
color: var(--text-color);
font-size: 14px;
text-align: center;
transition: 0.1s;
}
.neumorphism:active span {
transform: scale(0.95);
}
.neumorphism + .neumorphism{
margin-left: 30px;
}
</style>