HARMONYOS应用实例261:分段函数绘制
·
- 分段函数绘制
- 功能:定义分段函数规则,自动绘制不连续的函数图像。
- 支持创建多个分段函数,每个分段可以是不同类型
支持三种函数类型:一次函数、二次函数、常量函数
可调节每个分段的函数系数(a、b、c)
可设置每个分段的定义域(起点和终点)
可控制端点是否包含(开区间或闭区间)
自动绘制不连续的函数图像
支持显示选项控制(网格、坐标轴、标签)
可编辑和删除已有分段
// 分段函数绘制
// 功能:定义分段函数规则,自动绘制不连续的函数图像
// 分段数据接口
interface PiecewiseSegment {
id: number;
type: 'linear' | 'quadratic' | 'constant';
a: number;
b: number;
c: number;
start: number;
end: number;
includeStart: boolean;
includeEnd: boolean;
color: string;
}
// 端点数据接口
interface EndpointData {
x: number;
include: boolean;
}
@Entry
@Component
struct PiecewiseFunction {
@State canvasWidth: number = 350;
@State canvasHeight: number = 350;
@State showGrid: boolean = true;
@State showAxes: boolean = true;
@State showLabels: boolean = true;
@State segments: PiecewiseSegment[] = [];
@State selectedSegmentId: number = -1;
@State nextId: number = 1;
build() {
Column({ space: 15 }) {
Text('分段函数绘制')
.fontSize(26)
.fontWeight(FontWeight.Bold)
.textAlign(TextAlign.Center)
Column() {
Text('功能介绍')
.fontSize(18)
.fontWeight(FontWeight.Medium)
Text('定义分段函数规则,自动绘制不连续的函数图像,支持一次函数、二次函数和常量函数')
.fontSize(14)
.fontColor('#666666')
.textAlign(TextAlign.Center)
}
.width('100%')
.backgroundColor('#E3F2FD')
.borderRadius(10)
.padding(15)
Column({ space: 10 }) {
Text('函数图像')
.fontSize(18)
.fontWeight(FontWeight.Bold)
.width('100%')
.textAlign(TextAlign.Center)
Canvas(this.canvasContext)
.width(this.canvasWidth)
.height(this.canvasHeight)
.backgroundColor('#FFFFFF')
.border({ width: 2, color: '#333' })
.borderRadius(10)
.onReady(() => {
this.drawGraph()
})
}
.width('100%')
.padding(10)
.backgroundColor('#FAFAFA')
.borderRadius(10)
Column({ space: 10 }) {
Text('分段定义')
.fontSize(18)
.fontWeight(FontWeight.Bold)
.width('100%')
.textAlign(TextAlign.Center)
if (this.segments.length === 0) {
Column() {
Text('暂无分段,请添加分段')
.fontSize(14)
.fontColor('#999999')
}
.width('100%')
.padding(20)
.backgroundColor('#F5F5F5')
.borderRadius(8)
} else {
Column({ space: 8 }) {
ForEach(this.segments, (segment: PiecewiseSegment) => {
Column({ space: 5 }) {
Row({ space: 10 }) {
Text(`分段 ${segment.id}`)
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor(segment.color)
Text(this.getSegmentExpression(segment))
.fontSize(14)
.fontColor('#333333')
.layoutWeight(1)
Text('删除')
.fontSize(12)
.fontColor('#F44336')
.onClick(() => {
this.deleteSegment(segment.id)
})
}
.width('100%')
Text(`定义域: ${this.formatDomain(segment)}`)
.fontSize(12)
.fontColor('#666666')
}
.width('100%')
.padding(10)
.backgroundColor(segment.id === this.selectedSegmentId ? '#E3F2FD' : '#F5F5F5')
.borderRadius(8)
.border({ width: segment.id === this.selectedSegmentId ? 2 : 1, color: segment.color })
.onClick(() => {
this.selectedSegmentId = segment.id
})
})
}
.width('100%')
}
Button('添加分段')
.width('100%')
.height(40)
.fontSize(14)
.backgroundColor('#2196F3')
.onClick(() => {
this.addSegment()
})
}
.width('90%')
.padding(10)
.backgroundColor('#FAFAFA')
.borderRadius(10)
if (this.selectedSegmentId !== -1) {
this.SegmentEditor()
}
Column({ space: 8 }) {
Text('显示选项')
.fontSize(16)
.fontWeight(FontWeight.Medium)
Row({ space: 10 }) {
Row({ space: 5 }) {
Text('显示网格')
.fontSize(14)
Text(this.showGrid ? '✓' : '✗')
.fontSize(14)
.fontColor(this.showGrid ? '#4CAF50' : '#F44336')
}
.onClick(() => {
this.showGrid = !this.showGrid
this.drawGraph()
})
Row({ space: 5 }) {
Text('显示坐标轴')
.fontSize(14)
Text(this.showAxes ? '✓' : '✗')
.fontSize(14)
.fontColor(this.showAxes ? '#4CAF50' : '#F44336')
}
.onClick(() => {
this.showAxes = !this.showAxes
this.drawGraph()
})
Row({ space: 5 }) {
Text('显示标签')
.fontSize(14)
Text(this.showLabels ? '✓' : '✗')
.fontSize(14)
.fontColor(this.showLabels ? '#4CAF50' : '#F44336')
}
.onClick(() => {
this.showLabels = !this.showLabels
this.drawGraph()
})
}
.width('100%')
.justifyContent(FlexAlign.SpaceAround)
}
.width('90%')
.padding(10)
.backgroundColor('#F5F5F5')
.borderRadius(8)
Column({ space: 8 }) {
Text('使用说明')
.fontSize(16)
.fontWeight(FontWeight.Bold)
Text('• 点击"添加分段"创建新的函数分段')
.fontSize(14)
.fontColor('#666666')
Text('• 选择分段类型(一次、二次、常量)')
.fontSize(14)
.fontColor('#666666')
Text('• 调整函数系数和定义域')
.fontSize(14)
.fontColor('#666666')
Text('• 设置是否包含端点')
.fontSize(14)
.fontColor('#666666')
Text('• 观察分段函数的图像')
.fontSize(14)
.fontColor('#666666')
}
.width('95%')
.padding(12)
.backgroundColor('#FFF3E0')
.borderRadius(10)
Row({ space: 10 }) {
Button('重置')
.width('100%')
.height(50)
.fontSize(16)
.backgroundColor('#FF9800')
.onClick(() => {
this.reset()
})
}
.width('100%')
}
.width('100%')
.height('100%')
.padding(10)
.justifyContent(FlexAlign.Start)
}
private canvasContext: CanvasRenderingContext2D = new CanvasRenderingContext2D();
private addSegment() {
const colors = ['#2196F3', '#4CAF50', '#FF9800', '#9C27B0', '#F44336', '#00BCD4']
const color = colors[(this.segments.length) % colors.length]
const newSegment: PiecewiseSegment = {
id: this.nextId,
type: 'linear',
a: 1,
b: 0,
c: 0,
start: -2,
end: 2,
includeStart: true,
includeEnd: false,
color: color
}
this.segments.push(newSegment)
this.selectedSegmentId = this.nextId
this.nextId++
this.drawGraph()
}
private deleteSegment(id: number) {
this.segments = this.segments.filter(s => s.id !== id)
if (this.selectedSegmentId === id) {
this.selectedSegmentId = -1
}
this.drawGraph()
}
private updateSegmentType(id: number, type: 'linear' | 'quadratic' | 'constant') {
const segment = this.segments.find(s => s.id === id)
if (segment) {
segment.type = type
if (type === 'constant') {
segment.a = 0
segment.b = 0
}
this.drawGraph()
}
}
private updateSegmentParameter(id: number, param: string, value: number | boolean) {
const segment = this.segments.find(s => s.id === id)
if (segment) {
if (param === 'a') {
segment.a = value as number
} else if (param === 'b') {
segment.b = value as number
} else if (param === 'c') {
segment.c = value as number
} else if (param === 'start') {
segment.start = value as number
} else if (param === 'end') {
segment.end = value as number
} else if (param === 'includeStart') {
segment.includeStart = value as boolean
} else if (param === 'includeEnd') {
segment.includeEnd = value as boolean
}
this.drawGraph()
}
}
private getSegmentExpression(segment: PiecewiseSegment): string {
switch (segment.type) {
case 'linear':
return `${segment.a.toFixed(2)}x + ${segment.b.toFixed(2)}`
case 'quadratic':
return `${segment.a.toFixed(2)}x² + ${segment.b.toFixed(2)}x + ${segment.c.toFixed(2)}`
case 'constant':
return `${segment.c.toFixed(2)}`
}
}
private formatDomain(segment: PiecewiseSegment): string {
const startBracket = segment.includeStart ? '[' : '('
const endBracket = segment.includeEnd ? ']' : ')'
return `${startBracket}${segment.start.toFixed(2)}, ${segment.end.toFixed(2)}${endBracket}`
}
private drawGraph() {
const ctx = this.canvasContext
ctx.clearRect(0, 0, this.canvasWidth, this.canvasHeight)
if (this.showGrid) {
this.drawGridLines(ctx)
}
if (this.showAxes) {
this.drawAxes(ctx)
}
this.segments.forEach((segment: PiecewiseSegment) => {
this.drawSegment(ctx, segment)
})
}
private drawGridLines(ctx: CanvasRenderingContext2D) {
ctx.strokeStyle = '#E0E0E0'
ctx.lineWidth = 1
const centerX = this.canvasWidth / 2
const centerY = this.canvasHeight / 2
const scale = 30
for (let i = -5; i <= 5; i++) {
ctx.beginPath()
ctx.moveTo(centerX + i * scale, 0)
ctx.lineTo(centerX + i * scale, this.canvasHeight)
ctx.stroke()
}
for (let i = -5; i <= 5; i++) {
ctx.beginPath()
ctx.moveTo(0, centerY + i * scale)
ctx.lineTo(this.canvasWidth, centerY + i * scale)
ctx.stroke()
}
}
private drawAxes(ctx: CanvasRenderingContext2D) {
const centerX = this.canvasWidth / 2
const centerY = this.canvasHeight / 2
ctx.strokeStyle = '#333'
ctx.lineWidth = 2
ctx.beginPath()
ctx.moveTo(centerX, 0)
ctx.lineTo(centerX, this.canvasHeight)
ctx.stroke()
ctx.beginPath()
ctx.moveTo(0, centerY)
ctx.lineTo(this.canvasWidth, centerY)
ctx.stroke()
ctx.fillStyle = '#333'
ctx.font = '12px sans-serif'
ctx.fillText('x', this.canvasWidth - 15, centerY + 15)
ctx.fillText('y', centerX + 5, 15)
ctx.fillText('O', centerX + 5, centerY + 15)
const scale = 30
for (let i = -5; i <= 5; i++) {
if (i !== 0) {
ctx.fillText(i.toString(), centerX + i * scale - 3, centerY + 20)
ctx.fillText((-i).toString(), centerX - 15, centerY + i * scale + 3)
}
}
}
private drawSegment(ctx: CanvasRenderingContext2D, segment: PiecewiseSegment) {
const centerX = this.canvasWidth / 2
const centerY = this.canvasHeight / 2
const scale = 30
ctx.strokeStyle = segment.color
ctx.lineWidth = 3
ctx.beginPath()
let firstPoint = true
for (let x = segment.start; x <= segment.end; x += 0.05) {
let y: number
switch (segment.type) {
case 'linear':
y = segment.a * x + segment.b
break
case 'quadratic':
y = segment.a * x * x + segment.b * x + segment.c
break
case 'constant':
y = segment.c
break
}
const pixelX = centerX + x * scale
const pixelY = centerY - y * scale
if (firstPoint) {
ctx.moveTo(pixelX, pixelY)
firstPoint = false
} else {
ctx.lineTo(pixelX, pixelY)
}
}
ctx.stroke()
if (this.showLabels) {
const midX = (segment.start + segment.end) / 2
let midY: number
switch (segment.type) {
case 'linear':
midY = segment.a * midX + segment.b
break
case 'quadratic':
midY = segment.a * midX * midX + segment.b * midX + segment.c
break
case 'constant':
midY = segment.c
break
}
const pixelX = centerX + midX * scale
const pixelY = centerY - midY * scale
ctx.fillStyle = segment.color
ctx.font = '12px sans-serif'
ctx.fillText(`分段${segment.id}`, pixelX + 5, pixelY - 5)
}
this.drawEndpoints(ctx, segment)
}
private drawEndpoints(ctx: CanvasRenderingContext2D, segment: PiecewiseSegment) {
const centerX = this.canvasWidth / 2
const centerY = this.canvasHeight / 2
const scale = 30
const endpoint1: EndpointData = {
x: segment.start,
include: segment.includeStart
}
const endpoint2: EndpointData = {
x: segment.end,
include: segment.includeEnd
}
const endpoints: EndpointData[] = [endpoint1, endpoint2]
endpoints.forEach((endpoint: EndpointData) => {
let y: number
switch (segment.type) {
case 'linear':
y = segment.a * endpoint.x + segment.b
break
case 'quadratic':
y = segment.a * endpoint.x * endpoint.x + segment.b * endpoint.x + segment.c
break
case 'constant':
y = segment.c
break
}
const pixelX = centerX + endpoint.x * scale
const pixelY = centerY - y * scale
ctx.fillStyle = endpoint.include ? segment.color : '#FFFFFF'
ctx.strokeStyle = segment.color
ctx.lineWidth = 2
ctx.beginPath()
ctx.arc(pixelX, pixelY, endpoint.include ? 5 : 4, 0, 2 * Math.PI)
ctx.fill()
ctx.stroke()
})
}
private reset() {
this.segments = []
this.selectedSegmentId = -1
this.nextId = 1
this.showGrid = true
this.showAxes = true
this.showLabels = true
this.drawGraph()
}
@Builder
SegmentEditor() {
Column({ space: 10 }) {
Text('编辑分段')
.fontSize(18)
.fontWeight(FontWeight.Bold)
.width('100%')
.textAlign(TextAlign.Center)
Column({ space: 8 }) {
Text('函数类型')
.fontSize(14)
.fontColor('#666666')
Row({ space: 10 }) {
Button('一次函数')
.width('31%')
.height(35)
.fontSize(12)
.backgroundColor(this.segments.find(s => s.id === this.selectedSegmentId)?.type === 'linear' ? '#2196F3' : '#E0E0E0')
.onClick(() => {
this.updateSegmentType(this.selectedSegmentId, 'linear')
})
Button('二次函数')
.width('31%')
.height(35)
.fontSize(12)
.backgroundColor(this.segments.find(s => s.id === this.selectedSegmentId)?.type === 'quadratic' ? '#2196F3' : '#E0E0E0')
.onClick(() => {
this.updateSegmentType(this.selectedSegmentId, 'quadratic')
})
Button('常量函数')
.width('31%')
.height(35)
.fontSize(12)
.backgroundColor(this.segments.find(s => s.id === this.selectedSegmentId)?.type === 'constant' ? '#2196F3' : '#E0E0E0')
.onClick(() => {
this.updateSegmentType(this.selectedSegmentId, 'constant')
})
}
.width('100%')
.justifyContent(FlexAlign.SpaceBetween)
Column({ space: 8 }) {
Text(`a值: ${this.segments.find(s => s.id === this.selectedSegmentId)?.a.toFixed(2)}`)
.fontSize(14)
.fontColor('#666666')
Slider({
value: this.segments.find(s => s.id === this.selectedSegmentId)?.a || 0,
min: -2,
max: 2,
step: 0.1
})
.width('100%')
.blockColor('#2196F3')
.trackColor('#E0E0E0')
.selectedColor('#2196F3')
.onChange((value: number) => {
if (value === 0 && this.segments.find(s => s.id === this.selectedSegmentId)?.type === 'quadratic') {
value = 0.1
}
this.updateSegmentParameter(this.selectedSegmentId, 'a', value)
})
}
.width('100%')
.padding(8)
.backgroundColor('#F5F5F5')
.borderRadius(6)
Column({ space: 8 }) {
Text(`b值: ${this.segments.find(s => s.id === this.selectedSegmentId)?.b.toFixed(2)}`)
.fontSize(14)
.fontColor('#666666')
Slider({
value: this.segments.find(s => s.id === this.selectedSegmentId)?.b || 0,
min: -10,
max: 10,
step: 0.1
})
.width('100%')
.blockColor('#2196F3')
.trackColor('#E0E0E0')
.selectedColor('#2196F3')
.onChange((value: number) => {
this.updateSegmentParameter(this.selectedSegmentId, 'b', value)
})
}
.width('100%')
.padding(8)
.backgroundColor('#F5F5F5')
.borderRadius(6)
Column({ space: 8 }) {
Text(`c值: ${this.segments.find(s => s.id === this.selectedSegmentId)?.c.toFixed(2)}`)
.fontSize(14)
.fontColor('#666666')
Slider({
value: this.segments.find(s => s.id === this.selectedSegmentId)?.c || 0,
min: -10,
max: 10,
step: 0.1
})
.width('100%')
.blockColor('#2196F3')
.trackColor('#E0E0E0')
.selectedColor('#2196F3')
.onChange((value: number) => {
this.updateSegmentParameter(this.selectedSegmentId, 'c', value)
})
}
.width('100%')
.padding(8)
.backgroundColor('#F5F5F5')
.borderRadius(6)
Column({ space: 8 }) {
Text(`定义域起点: ${this.segments.find(s => s.id === this.selectedSegmentId)?.start.toFixed(2)}`)
.fontSize(14)
.fontColor('#666666')
Slider({
value: this.segments.find(s => s.id === this.selectedSegmentId)?.start || 0,
min: -5,
max: 5,
step: 0.1
})
.width('100%')
.blockColor('#4CAF50')
.trackColor('#E0E0E0')
.selectedColor('#4CAF50')
.onChange((value: number) => {
this.updateSegmentParameter(this.selectedSegmentId, 'start', value)
})
}
.width('100%')
.padding(8)
.backgroundColor('#F5F5F5')
.borderRadius(6)
Column({ space: 8 }) {
Text(`定义域终点: ${this.segments.find(s => s.id === this.selectedSegmentId)?.end.toFixed(2)}`)
.fontSize(14)
.fontColor('#666666')
Slider({
value: this.segments.find(s => s.id === this.selectedSegmentId)?.end || 0,
min: -5,
max: 5,
step: 0.1
})
.width('100%')
.blockColor('#4CAF50')
.trackColor('#E0E0E0')
.selectedColor('#4CAF50')
.onChange((value: number) => {
this.updateSegmentParameter(this.selectedSegmentId, 'end', value)
})
}
.width('100%')
.padding(8)
.backgroundColor('#F5F5F5')
.borderRadius(6)
Row({ space: 10 }) {
Row({ space: 5 }) {
Text('包含起点')
.fontSize(14)
Text(this.segments.find(s => s.id === this.selectedSegmentId)?.includeStart ? '✓' : '✗')
.fontSize(14)
.fontColor(this.segments.find(s => s.id === this.selectedSegmentId)?.includeStart ? '#4CAF50' : '#F44336')
}
.onClick(() => {
this.updateSegmentParameter(this.selectedSegmentId, 'includeStart', !this.segments.find(s => s.id === this.selectedSegmentId)?.includeStart)
})
Row({ space: 5 }) {
Text('包含终点')
.fontSize(14)
Text(this.segments.find(s => s.id === this.selectedSegmentId)?.includeEnd ? '✓' : '✗')
.fontSize(14)
.fontColor(this.segments.find(s => s.id === this.selectedSegmentId)?.includeEnd ? '#4CAF50' : '#F44336')
}
.onClick(() => {
this.updateSegmentParameter(this.selectedSegmentId, 'includeEnd', !this.segments.find(s => s.id === this.selectedSegmentId)?.includeEnd)
})
}
.width('100%')
.justifyContent(FlexAlign.SpaceAround)
}
.width('100%')
}
.width('90%')
.padding(10)
.backgroundColor('#FAFAFA')
.borderRadius(10)
}
}
更多推荐


所有评论(0)