wenmo8 发布的文章

多段区间的时间滑块slider实现方式

写在前面:今天要实现一个尖峰平谷的数据配置,这可一下难倒我了,但是还好互联网上大神云集,感谢各位大神的倾情分享,现在就写下我的感悟,留给看到这篇文章的你

参考链接--写在前面,希望好帖子和文章能够被更好的被发现

  1. 首先就是查看功能的大神贴,也是一切的基础

https://blog.csdn.net/qq_40950174/article/details/128829529

  1. 基于这个功能去实现时间滑块的参考

https://juejin.cn/post/7028885491617366053

  1. 组件参考:
    vue-range-multi

https://github.com/wiidede/vue-range-multi
这个组件简单易用而且超级方便
image

vue-slider-component
--
vue2的版本,个人喜欢这份文档,但是我用的vue3所以没有使用
https://github.com/NightCatSama/vue-slider-component

vue3-slider-component
最终参考:
https://github.com/s-sasaki-0529/vue-slider-component

实现:

有了前面的参考利器,写起来就方便很多了
思路:添加一个按钮,每次点击添加时,向内部添加一个容量为2的数组,然后通过数组的长度进行进度条渲染,在最后点击提交时对数组进行合并处理
我用的是个笨办法,如果有更好的办法,欢迎底下留言。

首先就是组件部分

<el-button type="primary" @click="addPoint">添加时间段</el-button>
    <el-button type="danger" @click="delPoint">删除时间段</el-button>
    <el-button type="success" @click="submit">提交</el-button>
    <VueSlider
        v-model="pointArr"
        :data="dataInit"
        :process="getProcess"
        :lazy="true"
        :adsorb="true"
        :drag-on-click="true"
        tooltip="hover"
    />

然后就是js部分

const pointArr = ref([0, 1])
const dataInit = ref([
      {value: 0, label: '0:00'},
      {value: 1, label: '1:00'},
      {value: 2, label: '2:00'},
      {value: 3, label: '3:00'},
      {value: 4, label: '4:00'},
      {value: 5, label: '5:00'},
      {value: 6, label: '6:00'},
      {value: 7, label: '7:00'},
      {value: 8, label: '8:00'},
      {value: 9, label: '9:00'},
      {value: 10, label: '10:00'},
      {value: 11, label: '11:00'},
      {value: 12, label: '12:00'},
      {value: 13, label: '13:00'},
      {value: 14, label: '14:00'},
      {value: 15, label: '15:00'},
      {value: 16, label: '16:00'},
      {value: 17, label: '17:00'},
      {value: 18, label: '18:00'},
      {value: 19, label: '19:00'},
      {value: 20, label: '20:00'},
      {value: 21, label: '21:00'},
      {value: 22, label: '22:00'},
      {value: 23, label: '23:00'},
    ]
)
//控制进度条的颜色
const getProcess = (pointArr:any) => {
  const newArr = []
  for (let i = 0; i < pointArr.length-1; i+=2) {
    newArr.push(([pointArr[i], pointArr[i + 1], {backgroundColor: 'pink'}]))
  }
  return newArr
}

//添加点位区间
const addPoint = () => {
  const length = pointArr.value.length;
  if (length < 2) {
    return;
  }
  const newArr = [pointArr.value.length, pointArr.value.length + 1]
  pointArr.value.push(...newArr);
  const lastIndex = pointArr.value.length - 2;
  const secondLastIndex = pointArr.value.length - 1;
  const arrRule = [pointArr.value[lastIndex], pointArr.value[secondLastIndex], {backgroundColor: 'pink'}];
  getProcess(pointArr).push(arrRule)
}

//删除上一个区间
const delPoint = () => {
  const length = pointArr.value.length;
  if (length <= 2) {
    return;
  }
  pointArr.value.pop();
  pointArr.value.pop();
  getProcess(pointArr).pop()
}

//合并区间
const submit = () => {
  console.log(pointArr.value);
  const result = [];

  // 创建一个数组来存储处理后的区间
  const processedIntervals = [];

  for (let i = 0; i < pointArr.value.length; i += 2) {
    const start = pointArr.value[i];
    const end = pointArr.value[i + 1];

    // 初始化一个新的区间
    let newInterval = [start, end];

    // 检查是否有交叉的区间需要合并
    if (processedIntervals.length > 0) {
      let lastInterval = processedIntervals[processedIntervals.length - 1];
      if (lastInterval[1] >= start) {
        // 有交叉,合并区间
        lastInterval[1] = Math.max(lastInterval[1], end);
      } else {
        // 无交叉,添加新区间
        processedIntervals.push(newInterval);
      }
    } else {
      // 第一个区间,直接添加
      processedIntervals.push(newInterval);
    }
  }

  // 处理 processedIntervals,生成最终的结果
  for (const interval of processedIntervals) {
    if (interval[0] === interval[1]) {
      result.push([interval[0]]);
    } else {
      result.push([interval[0], interval[1]]);
    }
  }
  console.log(result);
  return result;
}

最终实现:

  • 正常状态:
    image
    点击提交后:
    image

  • 重合合并:
    image
    点击提交后:
    image

一、需求背景

性能压测时,发现某接口存在性能瓶颈,期望借助工具定位该瓶颈,最好能定位至具体慢方法。

二、cProfile 简介

cProfile 是 Python 标准库中的一个模块,用于对 Python 程序进行性能分析,它能输出每个函数的调用次数、执行耗时等详细信息,可帮助开发者识别程序中运行缓慢的方法,以便进行性能优化,适合作为上述需求的解决方案。

此外,Python 还内置了使用纯 Python 实现的 profile 模块,与 cProfile 功能一样,只不过 cProfile 是用 C 语言编写,性能更高、开销更小,适合在性能敏感的环境(如线上生产环境)中使用。profile 是纯 Python 实现的模块,性能开销相对较大,但因其用 Python 编写,易于理解和修改,适合学习时使用。

三、使用方法

cProfile 支持三种使用方法,一是硬编码于代码中;二是在 Python 应用启动时加载 cProfile 模块;三是通过 IDE(PyCharm)运行。开发环境建议采用方法三,因其简单易用且结果图表丰富;生产环境建议采用方法二,此方法对代码无侵入性。

1. 硬编码于代码中

示例代码:

import cProfile

def my_function():
    # Some code to profile
    pass

profiler = cProfile.Profile()
profiler.enable()
my_function()
profiler.disable()
profiler.print_stats()

执行结果:

2 function calls in 0.000 seconds

Ordered by: standard name

ncalls  tottime  percall  cumtime  percall filename:lineno(function)
    1    0.000    0.000    0.000    0.000 test.py:3(my_function)
    1    0.000    0.000    0.000    0.000 {method 'disable' of '_lsprof.Profiler' objects}

结果字段说明:

ncalls:函数调用的次数。

tottime:在该函数中花费的总时间,不包括调用子函数的时间。

percall:tottime 除以 ncalls。

cumtime:该函数及其所有子函数中花费的总时间。

percall:cumtime 除以原始调用次数。

filename:lineno(function):函数所在的文件名、行号和函数名。

2. 在 Python 应用启动时加载 cProfile 模块

示例代码:

python -m cProfile my_script.py # 方法一、将结果输出至控制台
python -m cProfile -o output.prof my_script.py # 方法二、将结果保存到指定的prof文件

可使用 snakeviz 插件(安装方法为
pip install snakeviz
)分析 prof 文件。执行
snakeviz output.prof
后,会将结果挂载到 web 容器中,支持通过 URL(如
http://127.0.0.1:8080/snakeviz/xxxoutput.prof
)访问。

3. 通过 IDE(PyCharm)运行

使用方法:

通过菜单 [run] > [profile 'app'](其中 app 为应用名称,下同)启动应用。待应用执行完毕且停止后,相关面板会输出相应的调用统计与调用链。

调用统计:

img

表头“Name”表示被调用的模块或函数;“Call Count”表示被调用次数;“Time (ms)”表示耗时及百分比,时间单位为毫秒。

点击表头列名可对该列进行排序。

在调用统计中,选择“name”列的单元格,右键选中“Navigate to Source”或“Show on Call Graph”,可打开其源码或对应的调用链及位置。

调用链:

img

此外,通过菜单 [run] > [Concurrency Diagram 'app'] 启动程序,可查看到线程和异步协程(Asyncio)的调用情况,如下图所示:

img

四、相关配置项

1. cProfile

[root@test bin]# python3 -m cProfile -h
Usage: cProfile.py [-o output_file_path] [-s sort] [-m module | scriptfile] [arg] ...

Options:
  -h, --help            show this help message and exit
  -o OUTFILE, --outfile=OUTFILE
                        Save stats to <outfile> #  将分析结果输出到指定的文件中。
  -s SORT, --sort=SORT  Sort order when printing to stdout, based on pstats.Stats class # 指定输出结果的排序方式。可以根据不同的字段进行排序,如 time, cumulative, calls 等。
  -m                    Profile a library module # 分析一个模块,而不是一个脚本文件 

2. snakeviz

[root@test bin]# snakeviz --help
usage: snakeviz [-h] [-v] [-H ADDR] [-p PORT] [-b BROWSER_PATH] [-s] filename

Start SnakeViz to view a Python profile.

positional arguments:
  filename              Python profile to view

options:
  -h, --help            show this help message and exit
  -v, --version         show program \`s version number and exit
  -H ADDR, --hostname ADDR hostname to bind to (default: 127.0.0.1) # 用于指定绑定的主机名,默认值为 127.0.0.1,即本地主机。
  -p PORT, --port PORT  port to bind to; if this port is already in use a free port will be selected automatically (default: 8080) # 用于指定绑定的端口。如果指定的端口已被占用,程序将自动选择一个空闲端口。默认值为 8080。
  -b BROWSER_PATH, --browser BROWSER_PATH  name of webbrowser to launch as described in the documentation of Python\'s webbrowser module: https://docs.python.org/3/library/webbrowser.html # 按照 Python 的 webbrowser 模块的文档描述,指定要启动的浏览器名称。用户可以通过指定浏览器的路径来控制使用哪个浏览器打开应用。
  -s, --server          start SnakeViz in server-only mode--no attempt will be made to open a browser # 仅在服务器模式下启动 SnakeViz,不会尝试打开服务器中的浏览器。对于非图形化或不带浏览器的服务器非常有用。

五、生产环境使用示例

生产环境系统版本为 CentOS 7.9.2009 (Core),内核为 5.15.81,使用 4 核 4G 的容器运行
DB-GPT
controller 子系统。

1. 使用步骤

(1)执行脚本:
/usr/local/bin/python3.10 -m cProfile -o out.prof /usr/local/bin/dbgpt start controller &
,在 Python 应用启动时加载 cProfile 模块。

(2)执行相关接口压测。

(3)正常停止应用,生成性能分析结果文件(out.prof)。
注意:
性能分析结果只能是在程序正常停止后才能输出。常规两种做法:其一、后台守护进程,可以使用
kill -2 {应用 PID}
;其二、前台进程,通过 Ctrl + C 退出。

(4)使用
snakeviz -H 0.0.0.0 -s out.prof
分析结果文件(其中 -s 仅在服务端模式下运行,不会尝试打开服务器浏览器,一般情况下服务器不自带浏览器;-H 0.0.0.0,支持监听网卡所有接口),执行成功后,会输出可访问的 URL 地址,通过外部或本地浏览器打开。

2. 结果分析

使用外部或本地浏览器访问 snakeviz 生成的 URL 地址。结果如下:

结果说明:

(1)结果包含两部分,即上图和下表。上图展示选中方法及其子方法的调用关系、耗时及占比;下表展示所有方法及其总调用次数(ncalls)、方法本身总耗时(tottime)、方法本身平均耗时(percall)、方法及其子方法总耗时(cumtime)、方法及其子方法平均耗时(percall)以及方法所在文件位置及其行列号。

使用说明:

(1)表格任意列支持升降序操作,选中任意行,页面上方的图形会自动展示该方法及其子方法的调用关系、耗时及占比。

(2)单击图形中的任意子模块,可查看子模块所在方法及其子方法的调用关系、耗时及占比。

分析建议:

(1)选择 cumtime 列降序,选择入口代码,逐步细看,分析瓶颈点。

(2)使用 Sunburst 图样展示,更易体现各方法耗时占比。

3. 评估加载 cProfile 对性能的影响

我们用 Jmeter 对未加载以及加载 cProfile 模块的 Python 应用性能进行评估,以判断生产环境加载 cProfile 对性能的影响程度。结果如下:

配置 Jmeter 压测线程数 CPU 使用率 吞吐量 平均响应时间
CASE1 某应用未加载 cProfile 20 接近单核 100% 527 36ms
CASE2 某应用加载 cProfile 后 20 接近单核 100% 395 49ms

从上表可知,加载 cProfile 后,应用吞吐量下降 25%,平均响应时间增加 13ms,对性能有一定影响。

五、遇到问题

1. kill -15 {应用 PID} 无法生成性能分析结果文件

由于 cProfile 仅支持监听中断(SIGINT)信号,导致 kill 15 发送 SIGTERM 信号时,无法生成性能分析结果文件。

解决办法:使用 kill -2 {应用 PID}。

六、使用总结

(1)cProfile 可以生成详细的性能分布和调用链,非常适合作为分析和定位 Python 应用性能瓶颈的工具。

(2)由于生成性能分析结果文件需要停止应用,且对性能损耗较大(吞吐量降低 25%),所以一般情况下不建议在生产环境直接使用。不过可以使用
流量复制
,将生成环境的流量复制到测试或预生产环境,这样既能定位实际性能瓶颈,又不影响线上业务。

【引言】

在鸿蒙NEXT开发中,九宫格抽奖是一个常见且有趣的应用场景。通过九宫格抽奖,用户可以随机获得不同奖品,增加互动性和趣味性。本文将介绍如何使用鸿蒙开发框架实现九宫格抽奖功能,并通过代码解析展示实现细节。

【环境准备】

• 操作系统:Windows 10

• 开发工具:DevEco Studio NEXT Beta1 Build Version: 5.0.3.806

• 目标设备:华为Mate60 Pro

• 开发语言:ArkTS

• 框架:ArkUI

• API版本:API 12

【思路】

1. 奖品类定义

首先,我们定义了一个Prize类,用于表示每个奖品的信息,包括标题、颜色和描述。通过构造函数初始化奖品信息,为后续抽奖功能提供数据支持。

2. 抽奖页面结构

在抽奖页面结构中,我们使用了鸿蒙的组件化开发方式,定义了一个LotteryPage组件。该组件包含了抽奖所需的状态变量、抽奖顺序数组、奖品数组以及抽奖逻辑的实现方法。

3. 抽奖逻辑实现

九宫格抽奖的逻辑主要包括三个阶段:加速、匀速和减速。通过startLottery方法开始抽奖并逐渐加速,然后进入runAtConstantSpeed方法以恒定速度运行抽奖,最后通过slowDown方法减速并展示抽奖结果。

4. 构建UI界面

在构建UI界面时,我们使用了鸿蒙的布局组件和样式设置,将奖品以九宫格形式展示在页面上。每个奖品格子都可以点击,点击抽奖按钮后会触发抽奖动画,展示抽奖结果对话框。

【完整代码】

class Prize {
  title: string // 奖品标题
  color: string // 奖品颜色
  description: string // 奖品描述

  constructor(title: string, color: string, description: string = "") { // 构造函数,初始化奖品信息
    this.title = title // 设置奖品标题
    this.color = color // 设置奖品颜色
    this.description = description // 设置奖品描述
  }
}

// 九宫格抽奖图
@Entry // 入口注解,标识这是一个可启动的组件
@Component // 组件注解,标识这是一个组件
struct LotteryPage { // 定义抽奖页面结构
  @State selectedIndex: number = 0 // 当前选中的索引,初始为0
  private isAnimating: boolean = false // 是否正在进行动画,初始为false
  private selectionOrder: number[] = [0, 1, 2, 5, 8, 7, 6, 3] // 抽奖顺序数组
  private cellWidth: number = 200 // 单元格宽度
  private baseMargin: number = 10 // 单元格边距
  private prizeArray: Prize[] = [ // 奖品数组
    new Prize("红包", "#ff9675", "10元"), // 创建奖品对象
    new Prize("话费", "#ff9f2e", "5元"),
    new Prize("红包", "#8e7fff", "50元"),
    new Prize("红包", "#48d1ea", "30元"),
    new Prize("开始\n抽奖", "#fffdfd"), // 抽奖按钮
    new Prize("谢谢参与", "#5f5f5f"), // 参与提示
    new Prize("谢谢参与", "#5f5f5f"),
    new Prize("超市红包", "#5f5f5f", "100元"),
    new Prize("鲜花", "#75b0fe"), // 奖品对象
  ]
  private intervalID: number = 0 // 定时器ID,用于控制抽奖速度

  startLottery(speed: number = 500) { // 开始抽奖,默认速度为500毫秒
    setTimeout(() => { // 设置延时执行
      if (speed > 50) { // 如果速度大于50
        speed -= 50 // 减少速度
        this.startLottery(speed) // 递归调用,继续加速
      } else {
        this.runAtConstantSpeed() // 速度达到阈值,进入匀速阶段
        return
      }
      this.selectedIndex++ // 更新选中的索引
    }, speed) // 延迟时间为当前速度
  }

  runAtConstantSpeed() { // 以恒定速度运行抽奖
    let speed = 40 + Math.floor(Math.random() * this.selectionOrder.length) // 随机生成速度
    clearInterval(this.intervalID) // 清除之前的定时器
    this.intervalID = setInterval(() => { // 设置新的定时器
      if (this.selectedIndex >= speed) { // 如果选中索引达到速度
        clearInterval(this.intervalID) // 清除定时器
        this.slowDown() // 进入减速阶段
        return
      }
      this.selectedIndex++ // 更新选中的索引
    }, 50) // 每50毫秒更新一次
  }

  slowDown(speed = 50) { // 减速,默认速度为50毫秒
    setTimeout(() => { // 设置延时执行
      if (speed < 500) { // 如果速度小于500
        speed += 50 // 增加速度
        this.slowDown(speed) // 递归调用,继续减速
      } else {
        this.selectedIndex = this.selectedIndex % this.selectionOrder.length // 确保索引在范围内
        let index = this.selectionOrder[this.selectedIndex] // 获取当前选中的奖品索引
        this.isAnimating = false // 动画结束,设置为false
        this.getUIContext().showAlertDialog({ // 显示结果对话框
          title: '结果', // 对话框标题
          message: `${this.prizeArray[index].title}${this.prizeArray[index].description}`, // 显示奖品信息
          confirm: { // 确认按钮配置
            defaultFocus: true, // 默认聚焦
            value: '我知道了', // 按钮文本
            action: () => { // 按钮点击事件
            }
          },
          alignment: DialogAlignment.Center, // 对话框居中显示
        });
        return
      }
      this.selectedIndex++ // 更新选中的索引
    }, speed) // 延迟时间为当前速度
  }

  build() { // 构建UI
    Column() { // 使用列布局
      Flex({ wrap: FlexWrap.Wrap }) { // 使用弹性布局,允许换行
        ForEach(this.prizeArray, (item: Prize, index: number) => { // 遍历奖品数组
          Column() { // 每个奖品使用列布局
            Text(`${item.title}`) // 显示奖品标题
              .fontColor(this.selectionOrder[this.selectedIndex % this.selectionOrder.length] == index ? Color.White : // 根据选中状态设置字体颜色
              item.color)
              .fontSize(16) // 设置字体大小
            Text(`${item.description}`) // 显示奖品描述
              .fontColor(this.selectionOrder[this.selectedIndex % this.selectionOrder.length] == index ? Color.White : // 根据选中状态设置字体颜色
              item.color)
              .fontSize(20) // 设置字体大小
          }
          .clickEffect({ level: ClickEffectLevel.LIGHT, scale: 0.8 }) // 点击效果
          .onClick(() => { // 点击事件处理
            if (index == 4) { // 如果点击的是抽奖按钮
              if (this.isAnimating) { // 如果正在动画中,返回
                return
              }
              this.isAnimating = true // 设置为正在动画
              this.startLottery() // 开始抽奖
            }
          })
          .alignItems(HorizontalAlign.Center) // 水平居中对齐
          .justifyContent(FlexAlign.Center) // 垂直居中对齐
          .width(`${this.cellWidth}lpx`) // 设置单元格宽度
          .height(`${this.cellWidth}lpx`) // 设置单元格高度
          .margin(`${this.baseMargin}lpx`) // 设置单元格边距
          .backgroundColor(index == 4 ? "#ff5444" : // 设置背景颜色,抽奖按钮为特殊颜色
            (this.selectionOrder[this.selectedIndex % this.selectionOrder.length] == index ? Color.Gray : Color.White)) // 根据选中状态设置背景颜色
          .borderRadius(10) // 设置圆角
          .shadow({ // 设置阴影效果
            radius: 10, // 阴影半径
            color: "#f98732", // 阴影颜色
            offsetX: 0, // 水平偏移
            offsetY: 20 // 垂直偏移
          })
        })
      }.width(`${this.cellWidth * 3 + this.baseMargin * 6}lpx`) // 设置整体宽度
      .margin({ top: 30 }) // 设置顶部边距
    }
    .height('100%') // 设置高度为100%
    .width('100%') // 设置宽度为100%
    .backgroundColor("#ffb350") // 设置背景颜色
  }
}

当冬季的寒风拂过大地,冰雪经济如同一颗璀璨的明珠,在寒冷中散发着炽热的魅力。滑雪场、冰雕展、冰雪主题酒店等各类冰雪产业蓬勃发展,其背后的运营逻辑和策略,与 Java 设计模式有着奇妙的相似之处,为我们深入理解和运用 Java 设计模式提供了独特的视角。

一、工厂模式:冰雪项目的“生产基地”

在冰雪经济中,不同类型的冰雪娱乐项目就像是由工厂生产出来的产品。例如,一个大型冰雪乐园里有各种冰雪设施,如滑雪场、溜冰场、冰滑梯等。这里可以类比为 Java 的工厂模式,将创建这些不同冰雪项目的过程封装在一个“工厂”类中。

假设我们有一个
IceProjectFactory
类,它根据传入的参数来创建不同的冰雪项目对象。代码示例如下:

interface IceProject {
    void operate();
}

class SkiSlope implements IceProject {
    @Override
    public void operate() {
        System.out.println("滑雪场正在运营,人们在尽情滑雪。");
    }
}

class IceSkatingRink implements IceProject {
    @Override
    public void operate() {
        System.out.println("溜冰场热闹非凡,人们在冰面上翩翩起舞。");
    }
}

class IceProjectFactory {
    public static IceProject createProject(String type) {
        if ("ski".equals(type)) {
            return new SkiSlope();
        } else if ("skate".equals(type)) {
            return new IceSkatingRink();
        }
        return null;
    }
}

在这个例子中,
IceProjectFactory
就像是冰雪乐园的项目创建中心,根据需求创建不同的冰雪项目实例,这与工厂模式中通过工厂类来创建对象的思想一致,使得代码的创建逻辑更加清晰,易于维护和扩展。当冰雪乐园想要新增一种冰雪项目时,只需要在工厂类中添加相应的创建逻辑即可,而不会影响到其他部分的代码。

二、单例模式:冰雪经济中的“唯一资源管理”

在冰雪经济中,某些资源是独一无二且需要全局共享的,比如冰雪乐园中的造雪系统。整个乐园的雪质维护都依赖这一个造雪系统,它就如同 Java 中的单例模式。

以下是单例模式的示例代码:

class SnowMakingSystem {
    private static SnowMakingSystem instance;

    private SnowMakingSystem() {
        // 私有构造函数,防止外部直接创建实例
    }

    public static SnowMakingSystem getInstance() {
        if (instance == null) {
            synchronized (SnowMakingSystem.class) {
                if (instance == null) {
                    instance = new SnowMakingSystem();
                }
            }
        }
        return instance;
    }

    public void makeSnow() {
        System.out.println("造雪系统正在工作,为冰雪乐园制造雪花。");
    }
}

在这个代码中,
SnowMakingSystem
的构造函数被私有化,只能通过
getInstance
方法获取唯一的实例。这样可以确保在整个冰雪乐园的运营中,只有一个造雪系统在工作,避免了资源的浪费和冲突,就像在 Java 应用中,某些全局配置管理类或者数据库连接池等资源适合采用单例模式,保证资源的唯一性和一致性。

三、策略模式:冰雪旅游套餐的“灵活策略”

冰雪旅游企业常常会推出不同的旅游套餐,以满足不同游客的需求。例如,有经济型套餐、豪华型套餐、亲子型套餐等,每个套餐包含不同的服务组合和价格策略。这类似于 Java 的策略模式。

我们可以定义一个策略接口
TourPackageStrategy
,然后不同的套餐策略类实现这个接口:

interface TourPackageStrategy {
    void offerPackage();
}

class EconomyPackageStrategy implements TourPackageStrategy {
    @Override
    public void offerPackage() {
        System.out.println("提供经济型冰雪旅游套餐,包含基础的冰雪项目体验和简单住宿。");
    }
}

class LuxuryPackageStrategy implements TourPackageStrategy {
    @Override
    public void offerPackage() {
        System.out.println("提供豪华型冰雪旅游套餐,包含高端冰雪项目、豪华住宿和专属服务。");
    }
}

class FamilyPackageStrategy implements TourPackageStrategy {
    @Override
    public void offerPackage() {
        System.out.println("提供亲子型冰雪旅游套餐,包含适合家庭的冰雪娱乐项目和亲子互动活动。");
    }
}

然后,旅游企业可以根据游客的选择来应用不同的策略:

class TourCompany {
    private TourPackageStrategy strategy;

    public void setStrategy(TourPackageStrategy strategy) {
        this.strategy = strategy;
    }

    public void promotePackage() {
        strategy.offerPackage();
    }
}

通过这种策略模式,旅游公司可以轻松地切换不同的套餐策略,而不需要修改大量的代码。在 Java 应用中,当有多种算法或策略可以解决同一个问题时,策略模式可以让代码更加灵活和可维护,例如在电商系统中的不同促销策略或者支付方式的选择等场景中都可以应用。

四、观察者模式:冰雪赛事的“信息传播”

在冰雪赛事中,运动员的比赛成绩、赛事动态等信息需要及时传达给观众、媒体以及相关的体育机构。这可以类比为 Java 的观察者模式,运动员或赛事组织者作为被观察的对象,而观众、媒体等则是观察者。

首先定义一个观察者接口
IceEventObserver

interface IceEventObserver {
    void update(String eventInfo);
}

然后是被观察的赛事主题类
IceEventSubject

import java.util.ArrayList;
import java.util.List;

class IceEventSubject {
    private List<IceEventObserver> observers = new ArrayList<>();
    private String eventInfo;

    public void attachObserver(IceEventObserver observer) {
        observers.add(observer);
    }

    public void detachObserver(IceEventObserver observer) {
        observers.remove(observer);
    }

    public void setEventInfo(String eventInfo) {
        this.eventInfo = eventInfo;
        notifyObservers();
    }

    private void notifyObservers() {
        for (IceEventObserver observer : observers) {
            observer.update(eventInfo);
        }
    }
}

例如,有观众和媒体作为观察者:

class Audience implements IceEventObserver {
    @Override
    public void update(String eventInfo) {
        System.out.println("观众收到消息:" + eventInfo);
    }
}

class Media implements IceEventObserver {
    @Override
    public void update(String eventInfo) {
        System.out.println("媒体收到消息:" + eventInfo);
        // 媒体可能会进一步进行新闻报道等操作
    }
}

在实际应用中,当赛事中有新的情况发生,如运动员打破纪录时,赛事组织者可以通过
setEventInfo
方法更新信息,所有的观察者都会收到通知并做出相应的反应。在 Java 应用中,观察者模式常用于实现事件监听机制,如 GUI 编程中的按钮点击事件、消息队列中的消息处理等场景,能够有效地实现对象之间的解耦,提高系统的灵活性和可扩展性。

冰雪经济中的各种运营模式和场景为我们理解 Java 设计模式提供了生动的案例。通过将冰雪经济与 Java 设计模式相联系,我们可以更好地掌握这些设计模式的应用场景和优势,从而在 Java 编程中更加熟练地运用它们,打造出更加高效、灵活、可维护的软件系统,就像精心运营的冰雪产业一样,在不同的需求和环境下都能稳定而出色地运行。
作者:
代老师的编程课
出处:
https://zthinker.com/
如果你喜欢本文,请长按二维码,关注
Java码界探秘
.
代老师的编程课

前言

在OSG中,对于一些效果未被选中或者包含等业务,需要半透明效果来实现。
本篇描述OSG的半透明实现方式。


Demo

请添加图片描述


透明


功能概述

透明效果在三维场景中扮演着重要角色,它能够模拟玻璃、水体、烟雾等自然现象,增加场景的层次感和真实感。然而,透明效果的实现并非易事,它涉及到复杂的渲染技术和算法。OSG作为一个功能强大的场景图库,为透明效果的实现提供了强有力的支持。


材质属性的调整

在OSG中,实现透明效果的第一步是调整材质属性。材质属性决定了物体表面的外观特性,包括颜色、光泽度、反射率和透明度等。要实现透明效果,需要设置材质的透明度属性。
OSG中的osg::Material类用于设置物体的材质属性。通过调整osg::Material::TRANSPARENCY属性,我们可以控制物体的透明度。同时,我们还需要设置物体的颜色属性,并指定颜色的RGBA分量,其中A分量表示透明度。


深度测试的设置

深度测试是三维渲染中的一项重要技术,它用于确定物体在场景中的前后关系。在实现透明效果时,深度测试的设置尤为关键。需要确保深度测试是开启的,以便正确处理透明物体与背景或其他物体的遮挡关系。然而,由于透明物体具有部分遮挡的特性,还需要考虑深度写入(GL_DEPTH_WRITEMASK)的设置。在某些情况下,关闭深度写入可以避免透明物体渲染时的深度冲突问题。


渲染顺序的控制

透明物体的渲染顺序对其最终呈现效果具有重要影响。为了获得正确的渲染效果,我们需要确保透明物体按照从远到近的顺序进行渲染。OSG提供了透明排序机制来帮助我们实现这一目标。
通过设置osg::StateSet::TRANSPARENT_BIN渲染提示,我们可以将透明物体添加到单独的渲染队列中。OSG将按照从远到近的顺序渲染这些物体,从而确保渲染结果的正确性。


混合模式的应用

混合模式是实现透明效果的关键技术之一。它决定了透明物体与背景或其他物体混合时的颜色计算方式。在OSG中,我们可以通过设置osg::BlendFunc属性来指定混合模式。
常见的混合模式包括源颜色与目的颜色的加权和、源颜色与目的颜色的差值等。通过选择合适的混合模式,我们可以获得不同的透明效果。例如,使用GL_SRC_ALPHA和GL_ONE_MINUS_SRC_ALPHA作为混合因子,可以实现标准的透明度混合效果。
在OpenSceneGraph(OSG)中,实现透明效果通常涉及调整材质属性、深度测试设置以及渲染顺序。
要设置对象透明,是通过调整材质的透明度属性。osg::Material 类用于设置对象的材质属性,其中 osg::Material::TRANSPARENCY属性可以用于设置透明度。


基本实现流程

  • 创建材质实例,通过材质实现的(不是常规思维RGBA,因为A在此无效)
  • 材质实例设置材质颜色,材质颜色只有RGB有效,A无效
  • 设置材质实例的透明度
  • 获取模型(需要透明)的模型状态集
  • 状态集开启模型的深度测试
  • 状态集设置透明通道单独渲染
  • 状态集设置混合设置模式


注意事项

  • 确保透明对象在渲染队列中的顺序是正确的。OSG的透明排序机制可以帮助处理这个问题,但在某些复杂场景中,你可能需要手动控制渲染顺序。
  • 深度写入(GL_DEPTH_WRITEMASK)和深度测试(GL_DEPTH_TEST)的设置会影响透明对象的渲染效果。
  • 混合模式(osg::BlendFunc)的设置会影响透明对象与背景或其他对象的混合方式。

通过上述步骤,应该能够在OpenSceneGraph中实现基本的透明效果。如果需要更高级的透明处理,可以进一步探索OSG的渲染队列和混合模式设置。


透明实现步骤


步骤一:获取状态集

在这里插入图片描述

// 步骤一:获取状态集
osg::ref_ptr<osg::StateSet> pStateSet = pNode->getOrCreateStateSet();


步骤二:开启深度测试

在这里插入图片描述

// 步骤二:状态集 设置深度测试开启,确保透明的物体深度测试开启
pStateSet->setMode(GL_DEPTH_TEST, osg::StateAttribute::ON);


步骤三:创建材质实例

在这里插入图片描述

// 步骤三:创建材质实例
osg::ref_ptr<osg::Material> pMaterial = new osg::Material;


步骤四:设置材质颜色(理论上这的a无效)

在这里插入图片描述

// 步骤四:材质实例 设置材质颜色(RGB部分),透明度在颜色数组中设置
pMaterial->setDiffuse(osg::Material::FRONT_AND_BACK, osg::Vec4(color.x, color.y, color.z, color.a));


步骤五:设置材质透明度(理论上由这里控制透明度)

在这里插入图片描述

// 步骤五:材质实例 设置透明度(0-255): 设置了反倒没图形了
pMaterial->setTransparency(osg::Material::FRONT_AND_BACK, color.a * 255.0);
// pMaterial->setTransparency(osg::Material::FRONT_AND_BACK, 255.0);


步骤六:设置材质

在这里插入图片描述

// 步骤六:状态集 设置材质
pStateSet->setAttributeAndModes(pMaterial.get());


步骤七:设置透明通道单独渲染

在这里插入图片描述

// 步骤七:状态集 设置透明通道单独渲染
pStateSet->setRenderingHint(osg::StateSet::TRANSPARENT_BIN);


步骤八:设置渲染混合模式

在这里插入图片描述

// 步骤八:状态集 设置渲染混合模式
pStateSet->setAttributeAndModes(new osg::BlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA));


Demo源码


OsgManager.cpp相关函数代码

osg::ref_ptr<osg::Geode> OsgManager::createSphere(Point3F center, double radius, double ratio)
{
// 绘制球体
// 步骤一:创建一个用户保存几何信息的对象osg::Geode
osg::ref_ptr<osg::Geode> pGeode = new osg::Geode;
// 步骤二:创建专门指明精细度的类osg::TessellationHints,并设置对应精细度
osg::ref_ptr<osg::TessellationHints> pHints = new osg::TessellationHints;
pHints->setDetailRatio(ratio);
// 步骤三:绘制几何类型(几何体)
pGeode->addDrawable(new osg::ShapeDrawable(new osg::Sphere(osg::Vec3(center.x, center.y, center.y), radius), pHints));

return pGeode.get();
}

osg::ref_ptr<osg::Material> OsgManager::setTransparency(osg::Node *pNode, Point4F color)
{
#if 1
// 设置透明度

// 步骤一:获取状态集
osg::ref_ptr<osg::StateSet> pStateSet = pNode->getOrCreateStateSet();
// 步骤二:状态集 设置深度测试开启,确保透明的物体深度测试开启
pStateSet->setMode(GL_DEPTH_TEST, osg::StateAttribute::ON);
// 步骤三:创建材质实例
osg::ref_ptr<osg::Material> pMaterial = new osg::Material;
// 步骤四:材质实例 设置材质颜色(RGB部分),透明度在颜色数组中设置
pMaterial->setDiffuse(osg::Material::FRONT_AND_BACK, osg::Vec4(color.x, color.y, color.z, color.a));
// 步骤五:材质实例 设置透明度(0-255): 设置了反倒没图形了
// pMaterial->setTransparency(osg::Material::FRONT_AND_BACK, color.a * 255.0);
// pMaterial->setTransparency(osg::Material::FRONT_AND_BACK, 255.0);
// 步骤六:状态集 设置材质
pStateSet->setAttributeAndModes(pMaterial.get());
// 步骤七:状态集 设置透明通道单独渲染
pStateSet->setRenderingHint(osg::StateSet::TRANSPARENT_BIN);
// 步骤八:状态集 设置渲染混合模式
pStateSet->setAttributeAndModes(new osg::BlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA));
// static int z = 0;
// pStateSet->setRenderBinDetails(z++,QString("RenderBin%1").arg(z).toStdString());
#else
osg::ref_ptr<osg::Material> pMaterial = new osg::Material;
// Alpha混合开启
osg::ref_ptr<osg::StateSet> pStateSet = pNode->getOrCreateStateSet();
//取消深度测试
pStateSet->setMode(GL_BLEND,osg::StateAttribute::ON);
pStateSet->setMode( GL_DEPTH_TEST, osg::StateAttribute::OFF );
pStateSet->setMode( GL_LIGHTING, osg::StateAttribute::OFF | osg::StateAttribute::PROTECTED );
pStateSet->setRenderBinDetails(11, "RenderBin");
#endif
return pMaterial.get();
}


OsgWidget.cpp

osg::ref_ptr<osg::Node> OsgWidget::getTransparency()
{
// 其他demo的控件
updateControlVisible(false);

osg::ref_ptr<osg::Group> pGroup = new osg::Group();
{
// 创建几何体
osg::ref_ptr<osg::Geode> pGeode = OsgManager::createSphere(Point3F(0, 0, 0), 0.5);
// 设置透明度
osg::ref_ptr<osg::Material> pMaterial = OsgManager::setTransparency(pGeode, Point4F(1.0, 1.0, 1.0, 0.8));

pGroup->addChild(pGeode);
}
#if 0
{
// 创建几何体
osg::ref_ptr<osg::Geode> pGeode = OsgManager::createSphere(Point3F(-1, 0, 0), 0.5);
// 设置透明度
osg::ref_ptr<osg::Material> pMaterial = OsgManager::setTransparency(pGeode, Point4F(1.0, 0.0, 0.0, 0.25));

pGroup->addChild(pGeode);
}
{
// 创建几何体
osg::ref_ptr<osg::Geode> pGeode = OsgManager::createSphere(Point3F(1, 0, 0), 0.5);
// 设置透明度
osg::ref_ptr<osg::Material> pMaterial = OsgManager::setTransparency(pGeode, Point4F(0.0, 1.0, 0.0, 0.25));

pGroup->addChild(pGeode);
}
{
// 创建几何体
osg::ref_ptr<osg::Geode> pGeode = OsgManager::createSphere(Point3F(0, -1, 0), 0.5);
// 设置透明度
osg::ref_ptr<osg::Material> pMaterial = OsgManager::setTransparency(pGeode, Point4F(0.0, 0.0, 1.0, 0.50));


pGroup->addChild(pGeode);
}
{
// 创建几何体
osg::ref_ptr<osg::Geode> pGeode = OsgManager::createSphere(Point3F(0, 1, 0), 0.5);
// 设置透明度
osg::ref_ptr<osg::Material> pMaterial = OsgManager::setTransparency(pGeode, Point4F(1.0, 1.0, 0.0, 0.50));
pGroup->addChild(pGeode);
}
#endif

{
// 创建几何体
osg::ref_ptr<osg::Geode> pGeode = OsgManager::createSphere(Point3F(0, 0, -1), 0.5);
// 设置透明度
osg::ref_ptr<osg::Material> pMaterial = OsgManager::setTransparency(pGeode, Point4F(1.0, 0.0, 1.0, 0.5));
pGroup->addChild(pGeode);
}
{
// 创建几何体
osg::ref_ptr<osg::Geode> pGeode = OsgManager::createSphere(Point3F(0, 0, 1), 0.5);
// 设置透明度
osg::ref_ptr<osg::Material> pMaterial = OsgManager::setTransparency(pGeode, Point4F(0.0, 1.0, 1.0, 0.5));
pGroup->addChild(pGeode);
}

// 开启深度测试
// OsgManager::setDepthTest(pGroup, true);

// 关闭光照
// OsgManager::setLighting(pGroup.get(), false);

return pGroup.get();
}


工程模板v1.39.0

在这里插入图片描述


入坑


入坑一:设置透明后不显示


问题

设置透明后不显示
在这里插入图片描述


尝试

去掉透明度设置后,可以显示:
在这里插入图片描述

设置后就不显示,检查代码设置流程,并没有发现问题,然后查看了Demo代码,半透明也不显示;
在这里插入图片描述

到现在为止,笔者osg3.4.0的ming32版本种,旋转中心和半透明都有问题。
在这里插入图片描述

然后继续测试,发现设置透明度没用,但是设置透明颜色可以:
在这里插入图片描述


解决

在这里插入图片描述

在这里插入图片描述


入坑二:出现渲染截面


问题

出现渲染截面,测试只有球体、球面的时候才出现。
在这里插入图片描述

换个颜色:
在这里插入图片描述


原理

这是深度测试问题,单独开了每一个的深度测试,需要开这几个模型进行深度测试,开了深度测试也是一样,检查总代码是开了的,尝试下关闭所有深度测试,启动就有问题(开启深度测试,至少启动没有问题):
在这里插入图片描述

开启深度测试,关闭光照:
在这里插入图片描述

再次尝试打开stl球体模型,也是不行的,效果跟上面的一样,下面是绘制的stl球体:
在这里插入图片描述

在这里插入图片描述


解决

未解决,准备更换版本测试,经过多个版本都是一样。