光绘记的拍摄规划:项目管理与场景指南
如果你是光绘摄影爱好者,推荐去鸿蒙应用市场搜一下**「光绘记」**,下载体验体验。创建光绘项目、规划拍摄场景、管理光源道具,一套走下来对光绘摄影的创作流程会有更清晰的把控。体验完再回来看这篇文章,你会更清楚项目管理和场景指南背后是怎么实现的。
写在前面
大家好,我是一名写了十多年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。
接下来这篇文章,我会用"光绘记"的实际开发经历,带你看看光绘项目的管理、场景指南的实现、以及数据统计功能。
这篇文章聊什么
光绘记的拍摄规划功能,核心要解决三个问题:
- 项目管理:创建和管理光绘项目,追踪项目状态
- 场景指南:提供适合光绘的拍摄场景参考
- 数据统计:分析用户的光绘创作数据
第一步:项目管理
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 }
];
总结
这篇文章围绕"光绘记"的拍摄规划功能,讲解了三个核心主题:
- 项目管理:光绘项目的创建、状态流转、筛选展示
- 场景指南:8种适合光绘的拍摄场景,每个场景有具体的拍摄建议
- 数据统计:项目数、作品数、工具和技法使用分布的统计分析
项目管理的核心是状态流转------从规划中到准备就绪、拍摄中、已完成。场景指南帮助用户选择合适的拍摄地点,数据统计则让用户看到自己的创作历程。
如果你也是光绘摄影爱好者,希望这篇文章能帮你理解光绘记背后的项目管理逻辑。去鸿蒙应用市场下载体验一下吧,有问题欢迎交流。