l-circle.vue 9.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309
  1. <template>
  2. <view class="l-circle" :class="[{clockwise: !clockwise && !useCanvas}, ['is-' + lineCap]]" :style="[styles]">
  3. <!-- #ifndef APP-NVUE -->
  4. <view class="check"></view>
  5. <view v-if="!useCanvas" class="l-circle__trail" :style="[trailStyles]">
  6. <text class="cap start"></text>
  7. <text class="cap end"></text>
  8. </view>
  9. <view v-if="!useCanvas" class="l-circle__stroke" :style="[strokeStyles]">
  10. <view class="l-circle__stroke-line"></view>
  11. <text class="cap start" v-if="current"></text>
  12. <text class="cap end" v-if="current"></text>
  13. </view>
  14. <canvas v-if="useCanvas" type="2d" :canvas-id="canvasId" :id="canvasId" class="l-circle__canvas" ></canvas>
  15. <!-- #endif -->
  16. <!-- #ifdef APP-NVUE -->
  17. <web-view
  18. @pagefinish="finished = true"
  19. @error="onerror"
  20. @onPostMessage="onMessage"
  21. class="l-circle__view"
  22. ref="webview"
  23. src="/uni_modules/lime-circle/hybrid/html/index.html"></web-view>
  24. <!-- #endif -->
  25. <view class="l-circle__inner">
  26. <slot></slot>
  27. </view>
  28. </view>
  29. </template>
  30. <script lang="ts">
  31. // @ts-nocheck
  32. import { ref, computed, watch, reactive, defineComponent, onMounted ,onUnmounted, getCurrentInstance, nextTick } from './vue';
  33. import { useTransition } from './useTransition';
  34. import CircleProps from './props';
  35. import { getCanvas , isCanvas2d} from './getCanvas';
  36. import {Circle} from './circle'
  37. import { addUnit } from '@/uni_modules/lime-shared/addUnit';
  38. import { unitConvert } from '@/uni_modules/lime-shared/unitConvert';
  39. import { isString } from '@/uni_modules/lime-shared/isString';
  40. import { getRect } from '@/uni_modules/lime-shared/getRect';
  41. // import { useTransition } from '@/uni_modules/lime-use';
  42. export default defineComponent({
  43. name: 'l-circle',
  44. props: CircleProps,
  45. emits: ['update:current'],
  46. setup(props, {emit}) {
  47. const context = getCurrentInstance()
  48. const useCanvas = ref(props.canvas)
  49. const canvasId = `l-circle-${context.uid}`;
  50. let circleCanvas = null
  51. const RADIAN = Math.PI / 180
  52. const ratio = computed(() => 100 / props.max)
  53. const percent = ref<number>(0)
  54. const angle = computed(() => props.dashborad ? 135 : -90)
  55. const isShowCap = computed(() => {
  56. const { dashborad } = props
  57. return current.value > 0 && (dashborad ? true : current.value < props.max)
  58. })
  59. const offsetTop = ref<number | string>(0)
  60. const strokeEndCap = reactive({
  61. x: '0',
  62. y: '0'
  63. })
  64. const styles = computed(() => ({
  65. width: addUnit(props.size),
  66. height:addUnit(props.size),
  67. // #ifdef APP-NVUE
  68. transform: !useCanvas.value && `translateY(${offsetTop.value})`,
  69. // #endif
  70. // #ifndef APP-NVUE
  71. '--l-circle-offset-top': !useCanvas.value && offsetTop.value,
  72. // #endif
  73. }))
  74. const classes = computed(() => {
  75. const { clockwise, lineCap } = props
  76. // {
  77. // clockwise: !clockwise && !useCanvas.value,
  78. // [`is-${lineCap}`]: lineCap
  79. // }
  80. return lineCap ? `is-${lineCap} ` : ' ' + !clockwise && !useCanvas.value && `clockwise`
  81. })
  82. // css render
  83. const trailStyles = computed(() => {
  84. const { size, trailWidth, trailColor, dashborad } = props
  85. const circle = getCircle(size, trailWidth)
  86. const mask = `radial-gradient(transparent ${circle.r - 0.5}px, #000 ${circle.r}px)`
  87. let background = ''
  88. let capStart = { x: '', y: '' }
  89. let capEnd = capStart
  90. if (dashborad) {
  91. background = `conic-gradient(from 225deg, ${trailColor} 0%, ${trailColor} 75%, transparent 75%, transparent 100%)`
  92. capStart = calcPosition(circle.c, 135)
  93. capEnd = calcPosition(circle.c, 45)
  94. offsetTop.value = (unitConvert(size) - (unitConvert(capStart.y) + unitConvert(trailWidth) / 2)) / 4 + 'px'
  95. } else {
  96. background = `${trailColor}`
  97. }
  98. return {
  99. color: trailColor,
  100. mask,
  101. '-webkit-mask': mask,
  102. background,
  103. '--l-circle-trail-cap-start-x': capStart.x,
  104. '--l-circle-trail-cap-start-y': capStart.y,
  105. '--l-circle-trail-cap-end-x': capEnd.x,
  106. '--l-circle-trail-cap-end-y': capEnd.y,
  107. // '--l-circle-trail-cap-color': trailColor,
  108. '--l-circle-trail-cap-size': addUnit(unitConvert(trailWidth))
  109. }
  110. })
  111. const strokeStyles = computed(() => {
  112. const { size, strokeWidth, strokeColor, dashborad, max } = props
  113. const circle = getCircle(size, strokeWidth)
  114. const percent = dashborad ? current.value * 0.75 * ratio.value : current.value * ratio.value;
  115. const mask = `radial-gradient(transparent ${circle.r - 0.5}px, #000 ${circle.r}px)`
  116. const cap = calcPosition(circle.c, angle.value)
  117. let startColor = '';
  118. let endColor = '';
  119. let gradient = `conic-gradient(${dashborad ? 'from 225deg,' : ''} transparent 0%,`;
  120. let gradientEnd = `transparent var(--l-circle-percent), transparent ${dashborad ? '75%' : '100%'})`
  121. if (isString(strokeColor)) {
  122. gradient += ` ${strokeColor} 0%, ${strokeColor} var(--l-circle-percent), ${gradientEnd}`
  123. startColor = endColor = strokeColor
  124. } else {
  125. const len = strokeColor.length
  126. for (let i = 0; i < len; i++) {
  127. const color = strokeColor[i] as string
  128. if (i === 0) {
  129. gradient += `${color} 0%,`
  130. startColor = color
  131. } else {
  132. gradient += `${color} calc(var(--l-circle-percent) * ${(i + 1) / len}),`
  133. }
  134. if (i == len - 1) {
  135. endColor = color
  136. }
  137. }
  138. gradient += gradientEnd
  139. }
  140. return {
  141. mask,
  142. '-webkit-mask': mask,
  143. '--l-background': gradient,
  144. // background: isString(strokeColor) ? strokeColor : strokeColor[0],
  145. // background: gradient,
  146. // transition: `--l-circle-percent ${duration}ms`,
  147. '--l-circle-percent': `${percent / ratio.value == max ? percent + 0.1 : percent}%`,
  148. '--l-circle-stroke-cap-start-color': startColor,
  149. '--l-circle-stroke-cap-end-color': endColor,
  150. '--l-circle-stroke-cap-start-x': cap.x,
  151. '--l-circle-stroke-cap-start-y': cap.y,
  152. '--l-circle-stroke-cap-end-x': strokeEndCap.x,
  153. '--l-circle-stroke-cap-end-y': strokeEndCap.y,
  154. '--l-circle-stroke-cap-size': addUnit(unitConvert(strokeWidth)),
  155. '--l-circle-stroke-cap-opacity': isShowCap.value ? 1 : 0
  156. }
  157. })
  158. const calcStrokeCap = () => {
  159. const { size, strokeWidth, dashborad, max } = props
  160. const circle = getCircle(size, strokeWidth)
  161. const arc = dashborad ? 180 / 2 * 3 : 180 * 2
  162. const step = arc / max * current.value + angle.value
  163. const cap = calcPosition(circle.c, step)
  164. strokeEndCap.x = cap.x
  165. strokeEndCap.y = cap.y
  166. }
  167. const calcPosition = (r : number, angle : number) => {
  168. return {
  169. x: r + r * Math.cos(angle * RADIAN) + 'px',
  170. y: r + r * Math.sin(angle * RADIAN) + 'px'
  171. }
  172. }
  173. const getCircle = (size : number | string, lineWidth : number | string) => {
  174. const s = unitConvert(size)
  175. const w = unitConvert(lineWidth)
  176. const c = (s - w) / 2
  177. const r = s / 2 - w
  178. return {
  179. s, w, c, r
  180. }
  181. }
  182. // css render end
  183. const [current, stopTransition] = useTransition(percent, {
  184. duration: props.duration,
  185. })
  186. const stopPercent = watch(() => props.percent, (v) => {
  187. percent.value = v
  188. circleCanvas && circleCanvas.play(v)
  189. })
  190. const stopCurrent = watch(current, (v) => {
  191. if(!useCanvas.value) {
  192. calcStrokeCap()
  193. }
  194. emit('update:current', v.toFixed(2))
  195. })
  196. const getProps = () => {
  197. const {strokeWidth, trailWidth} = props
  198. return Object.assign({}, props, {trailWidth: unitConvert(trailWidth), strokeWidth: unitConvert(strokeWidth)})
  199. }
  200. // #ifdef APP-NVUE
  201. const finished = ref(false)
  202. const init = ref(false)
  203. const webview = ref(null)
  204. const onerror = () => {
  205. }
  206. const onMessage = (e: any) => {
  207. const {detail:{data: [res]}} = e;
  208. if(res.event == 'init') {
  209. useCanvas.value = res.data.useCanvas // && props.canvas
  210. init.value = true;
  211. circleCanvas = {
  212. setOption(props: any) {
  213. webview.value.evalJs(`setOption(${JSON.stringify(props)})`)
  214. },
  215. play(v: number) {
  216. webview.value.evalJs(`play(${v})`)
  217. }
  218. }
  219. }
  220. if(res.event == 'progress') {
  221. current.value = res.data
  222. }
  223. }
  224. let stopFinnished = watch(init, () => {
  225. stopFinnished()
  226. if(useCanvas.value) {
  227. circleCanvas.setOption(getProps())
  228. circleCanvas.play(props.percent)
  229. stopTransition()
  230. } else {
  231. webview.value.evalJs(`setClass('.l-circle', 'is-round', ${props.lineCap == 'round'})`)
  232. webview.value.evalJs(`setClass('.l-circle', 'clockwise', ${props.clockwise})`)
  233. stopFinnished = watch([trailStyles, strokeStyles], (v) => {
  234. webview.value.evalJs(`setStyle(0,${JSON.stringify(v[0])})`)
  235. webview.value.evalJs(`setStyle(1,${JSON.stringify(v[1])})`)
  236. }, { immediate: true })
  237. }
  238. percent.value = props.percent
  239. })
  240. // #endif
  241. // #ifndef APP-NVUE
  242. onMounted(() => {
  243. getRect('.check', {context}).then(res => {
  244. // alert(CSS.supports('background', 'conic-gradient(#000, #fff)'))
  245. useCanvas.value = !(res.height > 0 && !props.canvas)
  246. if(useCanvas.value) {
  247. stopTransition()
  248. setTimeout(() => {
  249. getCanvas(canvasId, {context}).then(res => {
  250. circleCanvas = new Circle(res, {
  251. size: unitConvert(props.size),
  252. run: (v: number) => current.value = v,
  253. pixelRatio: isCanvas2d ? uni.getSystemInfoSync().pixelRatio : 1,
  254. })
  255. circleCanvas.setOption(getProps())
  256. circleCanvas.play(props.percent)
  257. })
  258. },50)
  259. }
  260. percent.value = props.percent
  261. })
  262. })
  263. // #endif
  264. onUnmounted(() => {
  265. stopPercent()
  266. stopCurrent()
  267. stopTransition()
  268. // #ifdef APP-NVUE
  269. stopFinnished && stopFinnished()
  270. // #endif
  271. })
  272. return {
  273. useCanvas,
  274. canvasId,
  275. classes,
  276. styles,
  277. trailStyles,
  278. strokeStyles,
  279. current,
  280. // #ifdef APP-NVUE
  281. webview,
  282. onerror,
  283. onMessage,
  284. finished
  285. // #endif
  286. }
  287. }
  288. })
  289. </script>
  290. <style lang="scss">
  291. @import './index';
  292. </style>