鸿蒙APP开发-带你走进光绘记的拍摄规划

光绘记的拍摄规划:项目管理与场景指南

如果你是光绘摄影爱好者,推荐去鸿蒙应用市场搜一下**「光绘记」**,下载体验体验。创建光绘项目、规划拍摄场景、管理光源道具,一套走下来对光绘摄影的创作流程会有更清晰的把控。体验完再回来看这篇文章,你会更清楚项目管理和场景指南背后是怎么实现的。


写在前面

大家好,我是一名写了十多年Web前端的老兵。从jQuery时代一路走到React/Vue,CSS3动画、requestAnimationFrame、Web Animation API这些都算是看家本领。去年开始转战鸿蒙生态,用ArkTS开发App,这一路踩了不少坑,也积累了不少心得。

很多人觉得"前端转鸿蒙"应该很容易------都是写UI嘛,组件化、状态管理、生命周期,概念都差不多。但真正上手之后你会发现,相似的地方让你觉得亲切,不同的地方让你抓狂

比如:

  • 数据管理 :Web的localStorage在鸿蒙里变成了@ohos.data.preferences,从同步API变成了异步API,而且需要手动调用flush()才能真正写入磁盘。
  • 列表渲染 :React的map()在ArkTS里变成了ForEach,性能优化策略完全不同。
  • 路由导航 :React Router的useNavigate在鸿蒙里变成了router.pushUrl

接下来这篇文章,我会用"光绘记"的实际开发经历,带你看看光绘项目的管理、场景指南的实现、以及数据统计功能。


这篇文章聊什么

光绘记的拍摄规划功能,核心要解决三个问题:

  1. 项目管理:创建和管理光绘项目,追踪项目状态
  2. 场景指南:提供适合光绘的拍摄场景参考
  3. 数据统计:分析用户的光绘创作数据

第一步:项目管理

typescript 复制代码
interface LightProject {
  id: string;
  name: string;
  description: string;
  status: string;        // planning/ready/shooting/completed
  scene: string;         // 拍摄场景
  tools: string[];       // 使用的光源工具
  techniques: string[];  // 使用的技法
  presetId: string;      // 相机参数预设
  notes: string;
  createdAt: number;
  updatedAt: number;
}

// 场景定义
const SCENES = [
  { id: 'dark_room', name: '暗室', desc: '完全黑暗的室内空间', tips: '确保无漏光' },
  { id: 'outdoor_night', name: '户外夜晚', desc: '无光污染的户外', tips: '注意天气和月亮' },
  { id: 'tunnel', name: '隧道', desc: '封闭的隧道或走廊', tips: '注意安全' },
  { id: 'bridge', name: '桥梁', desc: '有栏杆的桥梁', tips: '利用栏杆做前景' },
  { id: 'rooftop', name: '楼顶', desc: '城市楼顶', tips: '注意风力' },
  { id: 'beach', name: '海滩', desc: '海边沙滩', tips: '利用水面反射' },
  { id: 'forest', name: '森林', desc: '密林深处', tips: '利用树木做框架' },
  { id: 'abandoned', name: '废弃建筑', desc: '废弃的工厂或建筑', tips: '注意安全和许可' }
];

// 技法定义
const TECHNIQUES = [
  { id: 'writing', name: '光写字', desc: '用光源在空中写字', difficulty: '初级' },
  { id: 'spinning', name: '旋转光', desc: '旋转光源形成圆形', difficulty: '初级' },
  { id: 'fiber', name: '光纤艺术', desc: '用光纤束创作', difficulty: '中级' },
  { id: 'painting', name: '光绘人像', desc: '在人像上绘制光效', difficulty: '中级' },
  { id: 'steel_wool', name: '钢丝棉', desc: '旋转燃烧的钢丝棉', difficulty: '高级' },
  { id: 'pixelation', name: '像素画', desc: '逐点绘制像素图案', difficulty: '高级' },
  { id: 'projection', name: '投影映射', desc: '将图案投影到物体上', difficulty: '高级' },
  { id: 'combination', name: '组合技法', desc: '多种技法组合使用', difficulty: '专家' }
];

项目管理页面:

typescript 复制代码
@Entry
@Component
struct ProjectManagePage {
  @State projects: LightProject[] = []
  @State filterStatus: string = 'all'

  async aboutToAppear() {
    await this.loadProjects()
  }

  async loadProjects() {
    const store = await preferences.getPreferences(getContext(), 'guanghuiji_data');
    const stored = await store.get('projects', '[]') as string;
    this.projects = JSON.parse(stored);
  }

  get filteredProjects(): LightProject[] {
    if (this.filterStatus === 'all') return this.projects;
    return this.projects.filter(p => p.status === this.filterStatus);
  }

  build() {
    Column() {
      // 状态筛选
      Row() {
        ForEach(['all', 'planning', 'ready', 'shooting', 'completed'], (status: string) => {
          Text(this.getStatusLabel(status))
            .fontSize(13)
            .padding(8)
            .borderRadius(8)
            .backgroundColor(this.filterStatus === status ? '#F59E0B' : '#374151')
            .fontColor(this.filterStatus === status ? '#FFF' : '#D1D5DB')
            .onClick(() => { this.filterStatus = status })
        })
      }
      .width('100%')
      .justifyContent(FlexAlign.SpaceAround)
      .margin({ bottom: 16 })

      // 项目列表
      List({ space: 12 }) {
        ForEach(this.filteredProjects, (project: LightProject) => {
          ListItem() {
            Column() {
              Row() {
                Text(project.name)
                  .fontSize(16)
                  .fontWeight(FontWeight.Bold)
                  .layoutWeight(1)
                Text(this.getStatusLabel(project.status))
                  .fontSize(11)
                  .padding(4)
                  .borderRadius(4)
                  .backgroundColor(this.getStatusColor(project.status))
              }
              .width('100%')

              Text(project.description)
                .fontSize(13)
                .fontColor('#9CA3AF')
                .margin({ top: 4 })

              // 标签
              Flex({ wrap: FlexWrap.Wrap }) {
                ForEach(project.tools, (tool: string) => {
                  Text(tool)
                    .fontSize(11)
                    .padding(4)
                    .margin(2)
                    .borderRadius(4)
                    .backgroundColor('#1F2937')
                })
              }
              .margin({ top: 8 })
            }
            .width('100%')
            .padding(12)
            .backgroundColor('#1F2937')
            .borderRadius(12)
          }
        })
      }
      .layoutWeight(1)

      // 新建项目按钮
      Button('新建光绘项目')
        .onClick(() => {
          router.pushUrl({ url: 'pages/AddProject' });
        })
        .width('100%')
        .backgroundColor('#F59E0B')
        .margin({ top: 12 })
    }
    .width('100%')
    .height('100%')
    .padding(16)
    .backgroundColor('#111827')
  }

  private getStatusLabel(status: string): string {
    const labels: Record<string, string> = {
      'all': '全部',
      'planning': '规划中',
      'ready': '准备就绪',
      'shooting': '拍摄中',
      'completed': '已完成'
    };
    return labels[status] || status;
  }

  private getStatusColor(status: string): string {
    const colors: Record<string, string> = {
      'planning': '#3B82F6',
      'ready': '#10B981',
      'shooting': '#F59E0B',
      'completed': '#6B7280'
    };
    return colors[status] || '#374151';
  }
}

第二步:数据统计

typescript 复制代码
@Entry
@Component
struct StatsPage {
  @State totalProjects: number = 0
  @State totalPaintings: number = 0
  @State toolDistribution: Record<string, number> = {}
  @State techniqueDistribution: Record<string, number> = {}
  @State sceneDistribution: Record<string, number> = {}

  private settings: RenderingContextSettings = new RenderingContextSettings(true)
  private ctx: CanvasRenderingContext2D = new CanvasRenderingContext2D(this.settings)

  async aboutToAppear() {
    await this.loadStats()
  }

  async loadStats() {
    const store = await preferences.getPreferences(getContext(), 'guanghuiji_data');

    const projectsStr = await store.get('projects', '[]') as string;
    const projects: LightProject[] = JSON.parse(projectsStr);
    this.totalProjects = projects.length;

    const paintingsStr = await store.get('paintings', '[]') as string;
    const paintings: LightPainting[] = JSON.parse(paintingsStr);
    this.totalPaintings = paintings.length;

    // 工具分布
    this.toolDistribution = {};
    projects.forEach(p => {
      p.tools.forEach(t => {
        this.toolDistribution[t] = (this.toolDistribution[t] || 0) + 1;
      });
    });

    // 技法分布
    this.techniqueDistribution = {};
    projects.forEach(p => {
      p.techniques.forEach(t => {
        this.techniqueDistribution[t] = (this.techniqueDistribution[t] || 0) + 1;
      });
    });
  }

  build() {
    Column() {
      // 概览卡片
      Row() {
        Column() {
          Text(`${this.totalProjects}`)
            .fontSize(28)
            .fontWeight(FontWeight.Bold)
            .fontColor('#F59E0B')
          Text('项目数')
            .fontSize(12)
            .fontColor('#9CA3AF')
        }
        .layoutWeight(1)

        Column() {
          Text(`${this.totalPaintings}`)
            .fontSize(28)
            .fontWeight(FontWeight.Bold)
            .fontColor('#F59E0B')
          Text('作品数')
            .fontSize(12)
            .fontColor('#9CA3AF')
        }
        .layoutWeight(1)
      }
      .width('100%')
      .padding(16)
      .backgroundColor('#1F2937')
      .borderRadius(12)

      // 工具使用排行
      Text('常用工具')
        .fontSize(16)
        .fontWeight(FontWeight.Medium)
        .margin({ top: 16, bottom: 8 })

      List({ space: 8 }) {
        ForEach(Object.entries(this.toolDistribution).sort((a, b) => b[1] - a[1]),
          ([tool, count]) => {
            ListItem() {
              Row() {
                Text(tool)
                  .fontSize(14)
                  .layoutWeight(1)
                Text(`${count}次`)
                  .fontSize(14)
                  .fontColor('#F59E0B')
              }
              .width('100%')
              .padding(8)
              .backgroundColor('#1F2937')
              .borderRadius(8)
            }
          })
      }
      .layoutWeight(1)
    }
    .width('100%')
    .height('100%')
    .padding(16)
    .backgroundColor('#111827')
  }
}

第三步:成就系统

typescript 复制代码
const ACHIEVEMENTS = [
  { id: 'first_project', name: '起步', desc: '创建第一个光绘项目', check: (s) => s.projectCount >= 1 },
  { id: 'first_painting', name: '第一幅', desc: '完成第一幅光绘作品', check: (s) => s.paintingCount >= 1 },
  { id: 'ten_paintings', name: '光绘新手', desc: '累计完成10幅作品', check: (s) => s.paintingCount >= 10 },
  { id: 'all_tools', name: '工具达人', desc: '使用过所有光源工具', check: (s) => s.usedTools >= 6 },
  { id: 'all_techniques', name: '技法大师', desc: '使用过所有光绘技法', check: (s) => s.usedTechniques >= 8 },
  { id: 'all_scenes', name: '场景探索者', desc: '在所有场景中拍摄过', check: (s) => s.usedScenes >= 8 },
  { id: 'night_owl', name: '夜行者', desc: '在凌晨0-4点完成作品', check: (s) => s.lateNightShots >= 5 },
  { id: 'ten_projects', name: '光绘达人', desc: '累计创建10个项目', check: (s) => s.projectCount >= 10 }
];

总结

这篇文章围绕"光绘记"的拍摄规划功能,讲解了三个核心主题:

  1. 项目管理:光绘项目的创建、状态流转、筛选展示
  2. 场景指南:8种适合光绘的拍摄场景,每个场景有具体的拍摄建议
  3. 数据统计:项目数、作品数、工具和技法使用分布的统计分析

项目管理的核心是状态流转------从规划中到准备就绪、拍摄中、已完成。场景指南帮助用户选择合适的拍摄地点,数据统计则让用户看到自己的创作历程。


如果你也是光绘摄影爱好者,希望这篇文章能帮你理解光绘记背后的项目管理逻辑。去鸿蒙应用市场下载体验一下吧,有问题欢迎交流。

相关推荐
陈_杨1 小时前
鸿蒙APP开发-带你走进光绘记的长曝光模拟
前端·javascript
陈_杨1 小时前
鸿蒙APP开发-带你走进节拍器的声音怎么这么准
前端·javascript
搬砖的阿wei1 小时前
Pinia 与 Vuex 区别
前端·vue.js
KaMeidebaby1 小时前
卡梅德生物技术快报|原核表达系统工艺优化:包涵体重折叠 + 分子筛纯化实现功能 RBD 高效制备,附全参数配置
前端·人工智能·算法·数据挖掘·数据分析
最爱睡觉睡觉睡觉1 小时前
代碼案例:CSS 屬性對照
前端·app
VitoChang2 小时前
开发体验超赞的SolidJS2.0来了
前端
CoCo的编程之路2 小时前
2026全栈演进:使用前端开发助手进行项目重构的最佳工具
大数据·前端·人工智能·ai编程·comate
@PHARAOH3 小时前
WHAT - NextAuth 权限认证机制
前端·微服务·服务端
掘金一周3 小时前
问卷调查:如果现在收到裁员通知,你手里的现金流能支撑多久? | 沸点周刊6.4
前端·人工智能·后端