1/* 2 * Copyright (c) 2022 Huawei Device Co., Ltd. 3 * Licensed under the Apache License, Version 2.0 (the "License"); 4 * you may not use this file except in compliance with the License. 5 * You may obtain a copy of the License at 6 * 7 * http://www.apache.org/licenses/LICENSE-2.0 8 * 9 * Unless required by applicable law or agreed to in writing, software 10 * distributed under the License is distributed on an "AS IS" BASIS, 11 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 * See the License for the specific language governing permissions and 13 * limitations under the License. 14 */ 15 16enum Type { 17 brightness, 18 saturate, 19 contrast 20} 21 22import { NavigationBar } from '../../common/components/navigationBar' 23import prompt from '@ohos.prompt'; 24 25@Entry 26@Component 27struct CanvasExample { 28 private settings: RenderingContextSettings = new RenderingContextSettings(true) 29 private ctx: CanvasRenderingContext2D = new CanvasRenderingContext2D(this.settings) 30 private img: ImageBitmap = new ImageBitmap('/common/test5.jpg') 31 @State visibility1: string = 'Visible' 32 @State visibility2: string = 'None' 33 @State brightnessValue: number = 10 34 @State oldBrightnessValue: number = 10 35 @State saturateValue: number = 10 36 @State oldSaturateValue: number = 10 37 @State contrastValue: number = 10 38 @State oldContrastValue: number = 10 39 @State typeProperties: Type = Type.brightness 40 @State clipOffsetx: number = 100 41 @State clipOffsety: number = 100 42 @State clipWidth: number = 100 43 @State clipHeight: number = 100 44 @State dWidth: number = 0 45 @State dHeight: number = 0 46 @State cropBoxLeftOne: number = 0 47 @State cropBoxLeftTwo: number = 0 48 @State cropBoxLeftThr: number = 0 49 @State cropBoxLeftFou: number = 0 50 @State cropBoxTopOne: number = 0 51 @State cropBoxTopTwo: number = 0 52 @State cropBoxTopThr: number = 0 53 @State cropBoxTopFou: number = 0 54 private brightnessImgData: any = null 55 private contrastImgData: any = null 56 private saturationImgData: any = null 57 @State clipState: string = 'original' 58 @State sliderChangeMode: SliderChangeMode = SliderChangeMode.Begin 59 @State adjustValue: number = 0 60 @State sliderNumber: string = '' 61 @State scaleX: number = 0 62 @State scaleY: number = 0 63 // 亮度 64 adjustBrightness(value) { 65 let imageData = this.ctx.getImageData 66 (this.clipOffsetx, this.clipOffsety, this.clipWidth * this.scaleX, this.clipHeight * this.scaleY) 67 this.ctx.putImageData((this.changeBrightness(imageData, value)), this.clipOffsetx, this.clipOffsety) 68 } 69 70 changeBrightness(imageData, value) { 71 let data = imageData.data 72 for (let i = 0; i < data.length; i += 4) { 73 const hsv = this.rgb2hsv([data[i], data[i + 1], data[i+2]]) 74 hsv[2] *= value 75 const rgb = this.hsv2rgb([...hsv]) 76 data[i] = rgb[0] 77 data[i+1] = rgb[1] 78 data[i+2] = rgb[2] 79 } 80 console.info('image:') 81 return imageData 82 } 83 // 对比度 84 adjustContrast(value) { 85 let imageData = this.ctx.getImageData 86 (this.clipOffsetx, this.clipOffsety, this.clipWidth * this.scaleX, this.clipHeight * this.scaleY) 87 this.ctx.putImageData((this.changeContrast(imageData, value)), this.clipOffsetx, this.clipOffsety) 88 } 89 90 changeContrast(imageData, value) { 91 const data = imageData.data 92 for (let i = 0;i < data.length; i += 4) { 93 const hsv = this.rgb2hsv([data[i], data[i+1], data[i+2]]) 94 hsv[0] *= value 95 const rgb = this.hsv2rgb([...hsv]) 96 data[i] = rgb[0] 97 data[i+1] = rgb[1] 98 data[i+2] = rgb[2] 99 } 100 return imageData 101 } 102 // 饱和度 103 adjustSaturation(value) { 104 let imageData = this.ctx.getImageData 105 (this.clipOffsetx, this.clipOffsety, this.clipWidth * this.scaleX, this.clipHeight * this.scaleY) 106 this.ctx.putImageData((this.changeSaturation(imageData, value)), this.clipOffsetx, this.clipOffsety) 107 } 108 109 changeSaturation(imageData, value) { 110 const data = imageData.data 111 for (let i = 0;i < data.length; i += 4) { 112 const hsv = this.rgb2hsv([data[i], data[i+1], data[i+2]]) 113 hsv[1] *= value 114 const rgb = this.hsv2rgb([...hsv]) 115 data[i] = rgb[0] 116 data[i+1] = rgb[1] 117 data[i+2] = rgb[2] 118 } 119 return imageData 120 } 121 // RGB转HSV 122 rgb2hsv(arr) { 123 let rr 124 let gg 125 let bb 126 const r = arr[0] / 255 127 const g = arr[1] / 255 128 const b = arr[2] / 255 129 let h 130 let s 131 const v = Math.max(r, g, b) 132 const diff = v - Math.min(r, g, b) 133 const diffc = function (c) { 134 return (v - c) / 6 / diff + 1 / 2 135 } 136 if (diff === 0) { 137 h = s = 0 138 } else { 139 s = diff / v 140 rr = diffc(r) 141 gg = diffc(g) 142 bb = diffc(b) 143 if (r === v) { 144 h = bb - gg 145 } else if (g === v) { 146 h = 1 / 3 + rr - bb 147 } else if (b === v) { 148 h = 2 / 3 + gg - rr 149 } 150 if (h < 0) { 151 h += 1 152 } else if (h > 1) { 153 h -= 1 154 } 155 } 156 return [Math.round(h * 360), Math.round(s * 100), Math.round(v * 100)] 157 } 158 // HSV转RGB 159 hsv2rgb(hsv) { 160 let _l = hsv[0] 161 let _m = hsv[1] 162 let _n = hsv[2] 163 let newR 164 let newG 165 let newB 166 if (_m === 0) { 167 _l = _m = _n = Math.round(255 * _n / 100) 168 newR = _l 169 newG = _m 170 newB = _n 171 } else { 172 _m = _m / 100 173 _n = _n / 100 174 const p = Math.floor(_l / 60) % 6 175 const f = _l / 60 - p 176 const a = _n * (1 - _m) 177 const b = _n * (1 - _m * f) 178 const c = _n * (1 - _m * (1 - f)) 179 switch (p) { 180 case 0: 181 newR = _n; 182 newG = c; 183 newB = a; 184 break; 185 case 1: 186 newR = b; 187 newG = _n; 188 newB = a; 189 break; 190 case 2: 191 newR = a; 192 newG = _n; 193 newB = c; 194 break; 195 case 3: 196 newR = a; 197 newG = b; 198 newB = _n; 199 break; 200 case 4: 201 newR = c; 202 newG = a; 203 newB = _n; 204 break; 205 case 5: 206 newR = _n; 207 newG = a; 208 newB = b; 209 break; 210 } 211 newR = Math.round(255 * newR) 212 newG = Math.round(255 * newG) 213 newB = Math.round(255 * newB) 214 } 215 return [newR, newG, newB] 216 } 217 218 build() { 219 Column(){ 220 NavigationBar({ title: 'Canvas' }) 221 222 Column() { 223 Column(){ 224 Column() { 225 Canvas(this.ctx) 226 .width(300) 227 .height(300) 228 .onAppear(() => { 229 this.dWidth = 300 230 this.dHeight = 250 231 // 绘制原始图片 232 this.ctx.drawImage(this.img, 0, 0, 300, 250) 233 }) 234 } 235 }.width('100%').justifyContent(FlexAlign.Center).alignItems(HorizontalAlign.Center) 236 } 237 .width('100%') 238 .constraintSize({ minHeight: 218, maxHeight: 402 }) 239 .padding({ left: 12, right: 12, top: 22, bottom: 22 }) 240 241 Column() { 242 Row({ space: 10 }) { 243 Button('原图') 244 .onClick(() => { 245 this.clipState = 'original' 246 // 原图裁剪大小与原图一致 247 this.clipWidth = this.dWidth 248 this.clipHeight = this.dHeight 249 this.clipOffsetx = (this.dWidth - this.clipWidth) / 2 250 this.clipOffsety = (this.dHeight - this.clipHeight) / 2 251 this.ctx.save() 252 }) 253 .fontSize('12fp') 254 .borderRadius(14) 255 .fontWeight(FontWeight.Medium) 256 // 1:1裁剪 257 Button('1:1') 258 .onClick(() => { 259 this.clipState = 'sameProportion' 260 this.clipWidth = this.dHeight 261 this.clipHeight = this.dHeight 262 this.cropBoxLeftTwo = (this.dHeight - this.clipHeight) / 2 263 this.cropBoxTopTwo = (this.dHeight - this.clipHeight) / 2 264 this.clipOffsetx = this.dHeight - this.clipWidth 265 this.clipOffsety = this.dHeight - this.clipHeight 266 this.scaleX = this.dWidth / this.clipWidth 267 this.scaleY = this.scaleX 268 }) 269 .fontSize('12fp') 270 .borderRadius(14) 271 .fontWeight(FontWeight.Medium) 272 // 16:9裁剪 273 Button('16:9') 274 .onClick(() => { 275 this.clipState = 'HighProportion' 276 this.clipHeight = (this.dWidth * 9 / 19) 277 this.clipWidth = this.dWidth 278 this.cropBoxLeftThr = (this.dWidth - this.clipWidth) / 2 279 this.cropBoxTopThr = (this.dHeight - this.clipHeight) / 2 280 this.clipOffsetx = (this.dWidth - this.clipWidth) / 2 281 this.clipOffsety = (300 - this.clipHeight) / 2 282 this.scaleX = this.dWidth / this.clipWidth 283 this.scaleY = this.scaleX 284 }) 285 .fontSize('12fp') 286 .borderRadius(14) 287 .fontWeight(FontWeight.Medium) 288 // 9:16裁剪 289 Button('9:16') 290 .onClick(() => { 291 this.clipState = 'LowProportion' 292 this.clipWidth = (this.dHeight * 9 / 16) 293 this.clipHeight = this.dHeight 294 this.cropBoxLeftFou = (300 - this.dHeight * 9 / 16) / 2 295 this.cropBoxTopFou = (this.dHeight - this.clipHeight) / 2 296 this.clipOffsetx = (300 - this.clipWidth) / 2 297 this.clipOffsety = this.cropBoxTopFou 298 this.scaleX = this.dHeight / this.clipHeight 299 this.scaleY = this.dWidth / this.clipHeight 300 }) 301 .fontSize('12fp') 302 .borderRadius(14) 303 .fontWeight(FontWeight.Medium) 304 } 305 .padding({ bottom: 5, top: 10 }) 306 .visibility(Visibility[this.visibility1]) 307 308 Row() { 309 Button('确认剪切') 310 .fontSize(18) 311 .onClick(() => { 312 if (this.clipState == 'original') { 313 this.ctx.save() 314 } 315 // 1:1剪切 316 if (this.clipState == 'sameProportion') { 317 let imageData = this.ctx.getImageData 318 (this.cropBoxLeftOne, this.cropBoxTopOne, this.clipWidth, this.clipHeight) 319 this.ctx.clearRect(0, 0, 300, 300) 320 this.ctx.scale(this.scaleX, this.scaleY) 321 this.ctx.putImageData(imageData, 0, 0) 322 this.brightnessImgData = this.ctx.getImageData 323 (this.clipOffsetx, this.clipOffsety, this.clipWidth * this.scaleX, this.clipHeight * this.scaleY) 324 this.contrastImgData = this.ctx.getImageData 325 (this.clipOffsetx, this.clipOffsety, this.clipWidth * this.scaleX, this.clipHeight * this.scaleY) 326 this.saturationImgData = this.ctx.getImageData 327 (this.clipOffsetx, this.clipOffsety, this.clipWidth * this.scaleX, this.clipHeight * this.scaleY) 328 this.ctx.save() 329 } 330 // 16:9剪切 331 if (this.clipState == 'HighProportion') { 332 let imageData = this.ctx.getImageData 333 (this.cropBoxLeftThr, this.cropBoxTopThr, this.clipWidth, this.clipHeight) 334 this.ctx.clearRect(0, 0, 300, 300) 335 this.ctx.scale(this.scaleX, this.scaleY) 336 this.ctx.putImageData(imageData, this.clipOffsetx, this.clipOffsety) 337 this.brightnessImgData = this.ctx.getImageData 338 (this.clipOffsetx, this.clipOffsety, this.clipWidth * this.scaleX, this.clipHeight * this.scaleY) 339 this.contrastImgData = this.ctx.getImageData 340 (this.clipOffsetx, this.clipOffsety, this.clipWidth * this.scaleX, this.clipHeight * this.scaleY) 341 this.saturationImgData = this.ctx.getImageData 342 (this.clipOffsetx, this.clipOffsety, this.clipWidth * this.scaleX, this.clipHeight * this.scaleY) 343 this.ctx.save() 344 } 345 // 9:16剪切 346 if (this.clipState == 'LowProportion') { 347 let dx = (300 - 250 * 9 / 16) / 2 348 let imageData = this.ctx.getImageData 349 (dx, this.cropBoxTopFou, this.clipWidth, this.clipHeight) 350 this.ctx.clearRect(0, 0, 300, 300) 351 this.ctx.scale(this.scaleX, this.scaleY) 352 this.ctx.putImageData(imageData, this.clipOffsetx, this.clipOffsety) 353 this.brightnessImgData = this.ctx.getImageData 354 (this.clipOffsetx, this.clipOffsety, this.clipWidth * this.scaleX, this.clipHeight * this.scaleY) 355 this.contrastImgData = this.ctx.getImageData 356 (this.clipOffsetx, this.clipOffsety, this.clipWidth * this.scaleX, this.clipHeight * this.scaleY) 357 this.saturationImgData = this.ctx.getImageData 358 (this.clipOffsetx, this.clipOffsety, this.clipWidth * this.scaleX, this.clipHeight * this.scaleY) 359 this.ctx.save() 360 } 361 }) 362 .fontSize('12fp') 363 .borderRadius(14) 364 .fontWeight(FontWeight.Medium) 365 .visibility(Visibility[this.visibility1]) 366 } 367 .margin({ top: 5, bottom: 5 }) 368 369 // 判断调节中选中亮度,对比度,饱和度三者中的哪一个 370 if (this.typeProperties == Type.brightness) { 371 Slider({ 372 value: this.brightnessValue, 373 min: 0, 374 max: 10, 375 step: 1.0, 376 style: SliderStyle.OutSet 377 }) 378 .onChange((value: number, mode: SliderChangeMode) => { 379 this.sliderNumber = mode.toString() 380 if (this.sliderNumber === '2') { 381 this.brightnessValue = value 382 if (value == 10) { 383 this.ctx.restore() 384 this.ctx.putImageData(this.brightnessImgData, this.clipOffsetx, this.clipOffsety) 385 } else { 386 const adjust = value / this.oldBrightnessValue 387 this.adjustValue = adjust 388 this.adjustBrightness(this.adjustValue) 389 } 390 } 391 }) 392 .visibility(Visibility[this.visibility2]) 393 } else if (this.typeProperties == Type.contrast) { 394 Slider({ 395 value: this.contrastValue, 396 min: 0, 397 max: 10, 398 step: 1.0, 399 style: SliderStyle.OutSet 400 }) 401 .onChange((value: number, mode: SliderChangeMode) => { 402 this.sliderNumber = mode.toString() 403 if (this.sliderNumber === '2') { 404 this.contrastValue = value 405 if (value == 10) { 406 this.ctx.restore() 407 this.ctx.putImageData(this.contrastImgData, this.clipOffsetx, this.clipOffsety) 408 } else { 409 const adjust = value / this.oldContrastValue 410 this.adjustValue = adjust 411 this.adjustContrast(this.adjustValue) 412 } 413 } 414 }) 415 } else { 416 Slider({ 417 value: this.saturateValue, 418 min: 0, 419 max: 10, 420 step: 1.0, 421 style: SliderStyle.OutSet 422 }) 423 .onChange((value: number, mode: SliderChangeMode) => { 424 this.sliderNumber = mode.toString() 425 if (this.sliderNumber === '2') { 426 this.saturateValue = value 427 if (value == 10) { 428 this.ctx.restore() 429 this.ctx.putImageData(this.saturationImgData, this.clipOffsetx, this.clipOffsety) 430 } else { 431 const adjust = value / this.oldSaturateValue 432 this.adjustValue = adjust 433 this.adjustSaturation(this.adjustValue) 434 } 435 } 436 }) 437 } 438 439 Row({ space: 10 }) { 440 Button('亮度') 441 .onClick(() => { 442 this.typeProperties = Type.brightness 443 }) 444 .fontSize('12fp') 445 .borderRadius(14) 446 .fontWeight(FontWeight.Medium) 447 Button('对比度') 448 .onClick(() => { 449 this.typeProperties = Type.contrast 450 }) 451 .fontSize('12fp') 452 .borderRadius(14) 453 .fontWeight(FontWeight.Medium) 454 Button('饱和度') 455 .onClick(() => { 456 this.typeProperties = Type.saturate 457 }) 458 .fontSize('12fp') 459 .borderRadius(14) 460 .fontWeight(FontWeight.Medium) 461 } 462 .padding({ bottom: 5, top: 5 }) 463 .visibility(Visibility[this.visibility2]) 464 465 Row({ space: 6 }) { 466 Button('裁剪') 467 .fontSize(18) 468 .onClick(() => { 469 this.visibility1 = 'Visible' 470 this.visibility2 = 'None' 471 this.clipState = 'original' 472 // 绘制图片 赋值裁剪框宽高 473 this.clipWidth = this.dWidth 474 this.clipHeight = this.dHeight 475 this.clipOffsetx = 0 476 this.clipOffsety = 0 477 // 绘制裁剪框 478 this.ctx.fillStyle = '#0344ee' 479 this.ctx.lineWidth = 5 480 this.cropBoxLeftOne = (this.dWidth - this.clipWidth) / 2 481 this.cropBoxTopOne = (this.dHeight - this.clipHeight) / 2 482 }) 483 .fontSize('12fp') 484 .borderRadius(14) 485 .fontWeight(FontWeight.Medium) 486 Button('调节') 487 .fontSize(18) 488 .onClick(() => { 489 this.visibility1 = 'None' 490 this.visibility2 = 'Visible' 491 this.typeProperties = Type.brightness 492 this.ctx.restore() 493 }) 494 .fontSize('12fp') 495 .borderRadius(14) 496 .fontWeight(FontWeight.Medium) 497 } 498 .padding({ bottom: 5, top: 5 }) 499 } 500 .padding(10) 501 .borderRadius(20) 502 .backgroundColor('#FFFFFF') 503 .margin({ top: 30 }) 504 } 505 .alignItems(HorizontalAlign.Center) 506 .justifyContent(FlexAlign.Start) 507 .width('100%') 508 .height('100%') 509 .backgroundColor('#F1F1F5') 510 .padding({ left: '3%', right: '3%' }) 511 } 512 513 pageTransition() { 514 PageTransitionEnter({ duration: 370, curve: Curve.Friction }) 515 .slide(SlideEffect.Bottom) 516 .opacity(0.0) 517 518 PageTransitionExit({ duration: 370, curve: Curve.Friction }) 519 .slide(SlideEffect.Bottom) 520 .opacity(0.0) 521 } 522}