2023-03-14 13:56

[OpenCV] 以最亮顏色的色相來進行灰階處理

OpenCV-Python 教學 https://docs.opencv.org/3.4.16/d6/d00/tutorial_py_root.html

原始圖片

原圖因為燈光散射的關係,整個圖片都出現發紅的現象


預設灰階 cv2.imread('01.jpg', cv2.IMREAD_GRAYSCALE)

有部分的色差變得不明顯,之後要進行辨識變得很困難


色相比例的灰階

透過色相的方式就好很多


HSV 的數值定義

  • H: 色相 0~360度
  • S: 飽和度 0~1
  • V: 明度 (黑)0~1(白)

OpenCV HSV 的數值定義

  • H: 0 ~ 180 => H / 2
  • S: 0 ~ 255 => S * 255
  • V: 0 ~ 255 => V * 255

相依套件:

  1. pip install opencv-python 
  2. pip install py-linq     # https://viralogic.github.io/py-enumerable/ 

程式:

  1. from py_linq import Enumerable 
  2. import cv2 
  3. import numpy as np 
  4.  
  5.  
  6. def get_hue(img): 
  7.    ''' 圖片色相 ''' 
  8.  
  9.    img = cv2.resize(img, (32, 32), interpolation = cv2.INTER_LINEAR) 
  10.    img_hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV) 
  11.    pixel_hsv = np.reshape(img_hsv,(-1,3)) 
  12.  
  13.    result = Enumerable(pixel_hsv)\ 
  14.        .order_by_descending(lambda p: p[2])\ 
  15.        .then_by_descending(lambda p: p[1])\ 
  16.        .first() 
  17.  
  18.    print(f'[result]: {result}') 
  19.    return result[0] 
  20.  
  21.  
  22.  
  23. def get_gray_rate(hue): 
  24.    ''' 根據色相計算灰階比例 ''' 
  25.  
  26.    hue_full = np.uint8([[[hue,255,255]]]) 
  27.    hue_bgr = cv2.cvtColor(hue_full, cv2.COLOR_HSV2BGR) 
  28.    (hue_b, hue_g, hue_r) = hue_bgr[0,0] 
  29.  
  30.    hue_total =  0.0 + hue_b + hue_g + hue_r 
  31.  
  32.    return [ 
  33.        hue_b / hue_total, 
  34.        hue_g / hue_total, 
  35.        hue_r / hue_total, 
  36.    ] 
  37.  
  38.  
  39.  
  40. img = cv2.imread('01.jpg') 
  41.  
  42. hue = get_hue(img) 
  43. gray_rate = get_gray_rate(hue) # 灰階比例 
  44.  
  45. print(f'hue:{hue}, gray_rate: {gray_rate}') 
  46.  
  47. # 灰階轉換 
  48. img_gray = cv2.transform(img, np.array([gray_rate])) 
  49.  
  50. cv2.imshow("img", img) 
  51. cv2.imshow("img_gray", img_gray) 
  52. cv2.waitKey(0) 
  53. cv2.destroyAllWindows() 
2023-03-14 13:23

[ThreeJS] 噴水粒子

要完成這個效果有三個問題要解決

  1. 自然的粒子分布
  2. 粒子的拋物線移動
  3. 粒子的擴散效果

我採用簡單的方式達成

  1. 用亂數產生分段的粒子團
  2. 用二次貝茲曲線建立拋物線
  3. 用粒子團的放大產生擴散效果

因為我用簡單的亂數分布粒子,粒子團會有立方體現象,球體分布會更好,但多個區段連起來就不明顯了。

二次貝茲曲線最大的困難在計算出結尾座標的位置,透過轉換到世界空間去定位出平面高度的座標,再轉換回物體空間來計算出結尾座標。

動畫用 AnimationMixer 去控制,只要定出每段粒子團的開始播放時間,粒子團就會接續的移動。

circle.png

程式:

  1. /* 初始化渲染器 */ 
  2. const renderer = new THREE.WebGLRenderer({ antialias: true }); 
  3. document.getElementById('Container').appendChild(renderer.domElement); 
  4. renderer.setPixelRatio(window.devicePixelRatio); 
  5. renderer.setSize(window.innerWidth, window.innerHeight); 
  6.  
  7. /* 初始化場景 */ 
  8. const scene = new THREE.Scene(); 
  9. scene.background = new THREE.Color('#111'); /* 背景顏色 */ 
  10. scene.add(new THREE.AmbientLight('#FFF', 0.5)); /* 加入環境光 */ 
  11.  
  12. /* 地面 */ 
  13. const floorMesh = new THREE.Mesh( 
  14.    new THREE.PlaneGeometry(200, 200), 
  15.    new THREE.MeshBasicMaterial({ color: '#DDD', depthWrite: false }) 
  16. ); 
  17. floorMesh.rotation.x = THREE.MathUtils.degToRad(-90); 
  18. scene.add(floorMesh); 
  19.  
  20.  
  21. /* 初始化鏡頭 */ 
  22. const camera = new THREE.PerspectiveCamera(40, window.innerWidth / window.innerHeight, 0.1, 300); 
  23. camera.position.set(0, 10, 22); 
  24.  
  25. /* 初始化軌道控制,鏡頭的移動 */ 
  26. const orbitControls = new THREE.OrbitControls(camera, renderer.domElement); 
  27. orbitControls.update(); 
  28.  
  29.  
  30. /* 動畫刷新器 */ 
  31. const mixers = []; 
  32.  
  33. function newMixer(target) { 
  34.    const mixer = new THREE.AnimationMixer(target); 
  35.    mixers.push(mixer); 
  36.    return mixer; 
  37. } 
  38.  
  39. /* 動畫計時器 */ 
  40. const clock = new THREE.Clock(); 
  41.  
  42. /* 渲染週期 */ 
  43. function renderCycle() { 
  44.    const delta = clock.getDelta(); 
  45.    mixers.forEach(x => x.update(delta)); 
  46.  
  47.    renderer.render(scene, camera); 
  48.    requestAnimationFrame(renderCycle); 
  49. } 
  50. renderCycle(); 
  51.  
  52.  
  53.  
  54. /*---------------------------------------------------------------*/ 
  55.  
  56. /* 水管物件,水滴粒子群組會附加在這裡 */ 
  57. const pipeMesh = new THREE.Mesh( 
  58.    new THREE.CylinderGeometry(0.5, 0.5, 9, 16), 
  59.    new THREE.MeshBasicMaterial( {color: '#eea236'} ) 
  60. ); 
  61. pipeMesh.rotation.x = THREE.MathUtils.degToRad(0); 
  62. pipeMesh.position.set(0, 4, 0); 
  63. scene.add(pipeMesh); 
  64.  
  65.  
  66. const dripGroup = new THREE.Group(); 
  67. dripGroup.position.set(0, 4, 0); 
  68. pipeMesh.add(dripGroup); 
  69.  
  70.  
  71. /* 水滴材質 */ 
  72. const dripMaterial = new THREE.PointsMaterial({ 
  73.    map: new THREE.TextureLoader().load('circle.png'), 
  74.    color: '#0aa', 
  75.    size: 1, 
  76.    opacity: 0.7, 
  77.    depthWrite: false, 
  78.    transparent: true, 
  79. }); 
  80.  
  81.  
  82. /* 建立水滴,用亂數建立粒子點 */ 
  83. for (let i = 0; i < 60; i++) { 
  84.    const vertices = []; 
  85.  
  86.    for (let j = 0; j < 40; j++) { 
  87.        const x = Math.random() - 0.5; 
  88.        const y = Math.random() - 0.5; 
  89.        const z = Math.random() - 0.5; 
  90.        vertices.push(x, y, z); 
  91.    } 
  92.  
  93.    const geometry = new THREE.BufferGeometry(); 
  94.    geometry.setAttribute('position', new THREE.Float32BufferAttribute(vertices, 3)); 
  95.  
  96.    const particles = new THREE.Points(geometry, dripMaterial); 
  97.    dripGroup.add(particles); 
  98.  
  99.    newMixer(particles); /* 水滴動畫 */ 
  100. } 
  101.  
  102.  
  103. /*---------------------------------------------------------------*/ 
  104.  
  105. const cycle = 2; /* 循環週期,會影響水流速度 */ 
  106. const scale = 8; /* 擴散大小 */ 
  107. const length = 14; /* 水流長度 */ 
  108.  
  109.  
  110. /* 二次貝茲曲線,用來取得拋物線的座標點 */ 
  111. const curve = new THREE.QuadraticBezierCurve3(); 
  112. curve.v1.set(0, length, 0); /* 曲線控制點座標 */ 
  113.  
  114.  
  115. /* 計算結尾座標 */ 
  116. const rootPos = dripGroup.getWorldPosition(new THREE.Vector3()); 
  117. const quaternion = dripGroup.getWorldQuaternion(new THREE.Quaternion()); 
  118. const toPos = curve.v1.clone(); 
  119. toPos.applyQuaternion(quaternion); /* 轉換到世界空間 */ 
  120.  
  121. /* 當水流是向上時,增加平面位置 */ 
  122. if (toPos.y > (length / 3)) { 
  123.    toPos.x *= 1.8; 
  124.    toPos.z *= 1.8; 
  125. } 
  126. toPos.y = Math.min(-rootPos.y, toPos.y * 1.5); /* 將結尾拉回平面高度 */ 
  127. toPos.applyQuaternion(quaternion.conjugate()); /* 轉換回物體空間 */ 
  128. curve.v2.copy(toPos); /* 曲線結尾點座標 */ 
  129.  
  130.  
  131. /* 建立拋物線及擴散動畫 */ 
  132. const points = curve.getPoints(10); /* 曲线分段 */ 
  133.  
  134. const curveTime = []; 
  135. const curvePos = []; 
  136. points.forEach((v, i) => { 
  137.    curveTime.push(cycle * i / points.length); 
  138.    curvePos.push(v.x, v.y, v.z); 
  139. }); 
  140. const posTrack = new THREE.VectorKeyframeTrack('.position', curveTime, curvePos); 
  141. const scaleTrack = new THREE.VectorKeyframeTrack('.scale', [0, cycle], [0, 0, 0, scale, scale, scale]); 
  142. const clip = new THREE.AnimationClip('scale', cycle, [posTrack, scaleTrack]); 
  143.  
  144. mixers.forEach((mixer, i) => { 
  145.    const action = mixer.clipAction(clip); 
  146.    action.time = cycle * i / mixers.length; 
  147.    action.play(); 
  148. }); 
2023-03-10 14:33

TypeScript 強轉型的地雷

用 Visual Studio 撰寫 TypeScript 一整個就很開心開心,只要好好的進行型態宣告,就可享受到跟強型別一樣的 IntelliSense 提示,實在太開心了,然後就大意了,忘記了其實骨子裡還是 JavaScript 這件事。

在取回後端的 json 資料的時候,貪圖方便就直接進行強轉型,哈哈!這裡存在不確定性,我就踩到地雷了!當然如果後端是可靠的,直接強轉型是不會有問題的。

我想都已經進行型別宣告了,難到不能自動轉型嗎?哈哈!在網路上找了很久都沒找到,找到的方式都需要二次宣定去做轉型。

簡單的展示一下這個問題,建立一個 any 的物件 a,然後強轉型成 DataModel 的物件 b,這時候 a 跟 b 其實還是同一個 Instance

  1. interface DataModel { 
  2.    id: number; 
  3.    status: string; 
  4. } 
  5.  
  6. let a: any = { id: '1', status: 3 }; 
  7.  
  8. let b: DataModel = a as DataModel; 

在存取 property 時都還是 a 的資料型態,這樣在調用該型態的方法時就會出錯。

為了型態的正確必須進行轉換:

  1. let b: DataModel = <DataModel>{ 
  2.    id: Number(a.id), 
  3.    status: String(a.status), 
  4. }; 
2023-03-08 16:45

[Angular] 簡單製作多國語系

我想要用一種簡單而且直覺的方式製作多國語系,想像的用法如下:

user-list.component.html

  1. <h2>{{lang['T_編輯使用者']}}</h2> 
  2.  
  3. <div> 
  4.    <p *ngIf="lang.id == 'en'"> 
  5.    Complicated English description... 
  6.    </p> 
  7.    <p *ngIf="lang.id == 'tw'"> 
  8.    複雜的中文描述... 
  9.    </p> 
  10. </div> 
  11.  
  12. <button type="submit">{{lang['B_儲存']}}</button> 
  13.  
  14. <select [(ngModel)]="lang.id" (change)="lang.change()"> 
  15.    <option *ngFor="let x of lang.options | keyvalue" [value]="x.key">{{x.value}}</option> 
  16. </select> 

我希望能使用中文變數,有熟悉的文字可以讓程式看起來親切一點,對於複雜的文字區段也保留彈性,簡單的切換方式,以及狀態保留,讓 browser 紀錄選擇的語系。

user-list.component.ts

  1. import { Component, OnInit } from '@angular/core'; 
  2. import { AppLang } from 'app/app.lang'; 
  3.  
  4. @Component({ 
  5.    selector: 'app-user-list', 
  6.    templateUrl: './user-list.component.html' 
  7. }) 
  8. export class UserListComponent implements OnInit { 
  9.  
  10.    /** 狀態選項 */ 
  11.    statusItems = { 
  12.        'y': this.lang.E_啟用, 
  13.        'n': this.lang.E_停用, 
  14.    }; 
  15.  
  16.    constructor( 
  17.        public lang: AppLang 
  18.    ) { } 
  19.  
  20.    ngOnInit() { 
  21.        let msg = String(this.lang.M_請填寫_0_的刻度值).format(3); 
  22.    } 
  23. } 

已經想好使用情境了,接著來就是完成實作了!

app.lang.ts

  1. import { Injectable } from '@angular/core'; 
  2.  
  3. /** 多國語言 */ 
  4. @Injectable() 
  5. export class AppLang { 
  6.  
  7.    constructor() { 
  8.        let _: any = {}; /* 語系字典 */ 
  9.        let self: any = this; 
  10.        let names: string[] = Object.keys(this).filter(n => !'id,options,change'.includes(n)); 
  11.  
  12.        /* 重組語系字典 */ 
  13.        Object.keys(this.options).forEach(id => { 
  14.            _[id] = {}; 
  15.            names.forEach(name => _[id][name] = self[name][id]); 
  16.        }); 
  17.  
  18.        /* 複寫 change */ 
  19.        this.change = function () { 
  20.            if (!_[this.id]) { this.id = 'tw'; } 
  21.  
  22.            Object.assign(this, _[this.id]); 
  23.            localStorage['langId'] = this.id; 
  24.        } 
  25.  
  26.        this.change (); 
  27.    } 
  28.  
  29.  
  30.    id: string = localStorage['langId']; 
  31.  
  32.    options: any = { 
  33.        tw: '中文', 
  34.        en: 'English', 
  35.    }; 
  36.  
  37.    /** 變更語言 */ 
  38.    change() { } 
  39.  
  40.  
  41.  
  42.    /*=[ Title ]=============================================*/ 
  43.  
  44.    T_編輯使用者: any = { 
  45.        tw: '編輯使用者', 
  46.        en: 'Edit User', 
  47.    }; 
  48.    //... 
  49.  
  50.    /*=[ Field ]=============================================*/ 
  51.  
  52.    F_名稱: any = { 
  53.        tw: '名稱', 
  54.        en: 'Name', 
  55.    }; 
  56.    //... 
  57.  
  58.    /*=[ Button ]=============================================*/ 
  59.  
  60.    B_儲存: any = { 
  61.        tw: '儲存', 
  62.        en: 'Save', 
  63.    }; 
  64.    //... 
  65.  
  66.  
  67.    /*=[ Enum ]=============================================*/ 
  68.  
  69.    E_啟用: any = { 
  70.        tw: '啟用', 
  71.        en: 'Enable', 
  72.    }; 
  73.    E_停用: any = { 
  74.        tw: '停用', 
  75.        en: 'Disable', 
  76.    }; 
  77.    //... 
  78.  
  79.  
  80.    /*=[ Message ]=============================================*/ 
  81.  
  82.    M_請填寫_0_的刻度值: any = { 
  83.        tw: '請填寫 {0} 的刻度值', 
  84.        en: 'Please fill in the scale value of {0}.', 
  85.    }; 
  86.    //... 
  87.  
  88. } 

app.module.ts 配置

  1. //... 
  2. import { AppLang } from 'app/app.lang'; 
  3.  
  4. @NgModule({ 
  5.    declarations: [ 
  6.        //... 
  7.    ], 
  8.    imports: [ 
  9.        //... 
  10.    ], 
  11.    providers: [ 
  12.        //... 
  13.        AppLang 
  14.    ], 
  15.    bootstrap: [AppComponent] 
  16. }) 
  17. export class AppModule { } 
2023-03-08 14:21

[Angular] 將 select, radio 改成一般等於 (==)

Angular 預設的 select, radio 比對是用全等於(===),這對用 string 的選項來說沒甚麼問題,但如果是數字選項就會出現型態不一致的問題,就必須型態轉換,這有點煩人,所以想要改成一般等於(==),在 Angular 的原始碼找到可以複寫判斷句的地方。

app.module.ts

  1. import { RadioControlValueAccessor, SelectControlValueAccessor } from '@angular/forms'; 
  2.  
  3. /* 將 SelectControlValueAccessor 的全等於改成一般等於 */ 
  4. SelectControlValueAccessor.prototype.compareWith = function (a, b) { 
  5.    return a == b; 
  6. }; 
  7.  
  8. /* 將 RadioControlValueAccessor 的全等於改成一般等於 */ 
  9. RadioControlValueAccessor.prototype.writeValue = function (value: any) { 
  10.    let self: any = this; 
  11.    self._state = value == this.value; 
  12.    self._renderer.setProperty(self._elementRef.nativeElement, 'checked', self._state); 
  13. }; 
2023-03-07 16:55

[ThreeJS] 單次動畫

  1. /* 單次動畫 */ 
  2. function animateOnce(mixer, prop, dur, values) { 
  3.    /* 停止 & 清除 先前的動畫 */ 
  4.    mixer.stopAllAction(); 
  5.    mixer.uncacheRoot(mixer.getRoot()); 
  6.  
  7.    /* 增加動畫片段 */ 
  8.    const track = new THREE.KeyframeTrack(prop, [0, dur], values); 
  9.    const clip = new THREE.AnimationClip('move', dur, [track]); 
  10.    const action = mixer.clipAction(clip); 
  11.    action.clampWhenFinished = true; 
  12.    action.setLoop(THREE.LoopOnce); 
  13.  
  14.    /* Promise 來處理完成事件,這樣就可以用 await */ 
  15.    return new Promise((resolve) => { 
  16.        const finished = function () { 
  17.            mixer.removeEventListener('finished', finished); 
  18.            resolve(); 
  19.        }; 
  20.        mixer.addEventListener('finished', finished); 
  21.  
  22.        action.play(); /* 播放動畫 */ 
  23.    }); 
  24. } 

想要能像 SVG 一樣簡單的控制模型的移動,ThreeJS 並沒有直接提供我想要的模式,試了很久終於成功了。

mixer 中的 action 是不斷添加的,這會混亂播放動畫,所以在播放新的動畫前必須[停止]且[清除]先前的動畫。

為了可以簡單串接不同模型的動畫,所以就想用 await 來達成,所以加上 Promise 的包裝。

完整程式

  1. /* 初始化渲染器 */ 
  2. const renderer = new THREE.WebGLRenderer({ antialias: true }); 
  3. document.getElementById('Container').appendChild(renderer.domElement); 
  4. renderer.setPixelRatio(window.devicePixelRatio); 
  5. renderer.setSize(window.innerWidth, window.innerHeight); 
  6.  
  7. /* 初始化場景 */ 
  8. const scene = new THREE.Scene(); 
  9. scene.background = new THREE.Color('#000'); /* 背景顏色 */ 
  10. scene.add(new THREE.AmbientLight('#FFF', 0.5)); /* 加入環境光 */ 
  11. scene.add(new THREE.AxesHelper(50)); /* 3D 軸標示 */ 
  12.  
  13. /* 初始化鏡頭 */ 
  14. const camera = new THREE.PerspectiveCamera(40, window.innerWidth / window.innerHeight, 0.1, 300); 
  15. camera.position.set(0, 4, 12); 
  16.  
  17. /* 初始化軌道控制,鏡頭的移動 */ 
  18. const orbitControls = new THREE.OrbitControls(camera, renderer.domElement); 
  19. orbitControls.update(); 
  20.  
  21.  
  22. /* 動畫刷新器 */ 
  23. const mixers = []; 
  24.  
  25. function newMixer(target) { 
  26.    const mixer = new THREE.AnimationMixer(target); 
  27.    mixers.push(mixer); 
  28.    return mixer; 
  29. } 
  30.  
  31. /* 動畫計時器 */ 
  32. const clock = new THREE.Clock(); 
  33.  
  34. /* 渲染週期 */ 
  35. function renderCycle() { 
  36.    const delta = clock.getDelta(); 
  37.    mixers.forEach(x => x.update(delta)); 
  38.  
  39.    renderer.render(scene, camera); 
  40.    requestAnimationFrame(renderCycle); 
  41. } 
  42. renderCycle(); 
  43.  
  44.  
  45.  
  46. /*---------------------------------------------------------------*/ 
  47.  
  48. /* 單次動畫 */ 
  49. function animateOnce(mixer, prop, dur, values) { 
  50.    /* 停止 & 清除 先前的動畫 */ 
  51.    mixer.stopAllAction(); 
  52.    mixer.uncacheRoot(mixer.getRoot()); 
  53.  
  54.    /* 增加動畫片段 */ 
  55.    const track = new THREE.KeyframeTrack(prop, [0, dur], values); 
  56.    const clip = new THREE.AnimationClip('move', dur, [track]); 
  57.    const action = mixer.clipAction(clip); 
  58.    action.clampWhenFinished = true; 
  59.    action.setLoop(THREE.LoopOnce); 
  60.  
  61.    /* Promise 來處理完成事件,這樣就可以用 await */ 
  62.    return new Promise((resolve) => { 
  63.        const finished = function () { 
  64.            mixer.removeEventListener('finished', finished); 
  65.            resolve(); 
  66.        }; 
  67.        mixer.addEventListener('finished', finished); 
  68.  
  69.        action.play(); /* 播放動畫 */ 
  70.    }); 
  71. } 
  72.  
  73.  
  74. const cubeA = new THREE.Mesh( 
  75.    new THREE.BoxGeometry(1, 1, 1), 
  76.    new THREE.MeshBasicMaterial( {color: '#0F0'} ) 
  77. ); 
  78. const mixerA = newMixer(cubeA); 
  79. scene.add(cubeA); 
  80.  
  81.  
  82. const cubeB = new THREE.Mesh( 
  83.    new THREE.BoxGeometry(1, 1, 1), 
  84.    new THREE.MeshBasicMaterial( {color: '#00F'} ) 
  85. ); 
  86. const mixerB = newMixer(cubeB); 
  87. scene.add(cubeB); 
  88.  
  89.  
  90. async function run() { 
  91.    await animateOnce(mixerA, '.position[x]', 3, [0, 3]); 
  92.    await animateOnce(mixerB, '.position[y]', 3, [0, 2]); 
  93.    await animateOnce(mixerA, '.position[x]', 3, [3, 0]); 
  94.    await animateOnce(mixerB, '.position[y]', 3, [2, 0]); 
  95. } 
  96.  
  97. run(); 
2023-03-07 16:06

[ThreeJS] 用 SVG 貼圖顯示中文

HTML

  1. <div id="Container"></div> 
  2.  
  3. <div id="LabelTpl" style="display:none;"> 
  4.    <svg xmlns="http://www.w3.org/2000/svg" width="1024" height="1024" viewBox="0 0 30 30"> 
  5.        <text x="15" y="15" fill="#fff" text-anchor="middle"></text> 
  6.    </svg> 
  7. </div> 
  8.  
  9. <script src="../Scripts/three.145/three.min.js"></script> 
  10. <script src="../Scripts/three.145/controls/OrbitControls.js"></script> 
  11. <script src="../Scripts/three.145/loaders/GLTFLoader.js"></script> 
  12. <script src="main.js"></script> 

main.js

  1. /* 初始化渲染器 */ 
  2. const renderer = new THREE.WebGLRenderer({ antialias: true }); 
  3. document.getElementById('Container').appendChild(renderer.domElement); 
  4.  
  5. renderer.setPixelRatio(window.devicePixelRatio); 
  6. renderer.setSize(window.innerWidth, window.innerHeight); 
  7.  
  8.  
  9. /* 初始化場景 */ 
  10. const scene = new THREE.Scene(); 
  11. scene.background = new THREE.Color('#000'); /* 背景顏色 */ 
  12. scene.add(new THREE.AmbientLight('#FFF', 0.5)); /* 加入環境光 */ 
  13. scene.add(new THREE.AxesHelper(50)); /* 3D 軸標示 */ 
  14.  
  15. /* 初始化鏡頭 */ 
  16. const camera = new THREE.PerspectiveCamera(40, window.innerWidth / window.innerHeight, 0.1, 300); 
  17. camera.position.set(0, 4, 12); 
  18.  
  19.  
  20. /* 初始化軌道控制,鏡頭的移動 */ 
  21. const orbitControls = new THREE.OrbitControls(camera, renderer.domElement); 
  22. orbitControls.update(); 
  23.  
  24.  
  25. /* 渲染週期 */ 
  26. function renderCycle() { 
  27.    renderer.render(scene, camera); 
  28.    requestAnimationFrame(renderCycle); 
  29. } 
  30. renderCycle(); 
  31.  
  32.  
  33.  
  34. /*---------------------------------------------------------------*/ 
  35.  
  36. function svgBase64(svg) { 
  37.    svg = svg.replace(/\s+</g, '<').replace(/>\s+/g, '>'); 
  38.    return 'data:image/svg+xml,' + encodeURIComponent(svg); 
  39. } 
  40.  
  41.  
  42. const textureLoader = new THREE.TextureLoader(); 
  43. const $labelTpl = document.querySelector('#LabelTpl'); 
  44. const $labelText = document.querySelector('#LabelTpl text'); 
  45.  
  46.  
  47. /* 環線 */ 
  48. function addCircle(label,  z) { 
  49.    $labelText.innerHTML = label; 
  50.    let imageData = svgBase64($labelTpl.innerHTML); 
  51.  
  52.    const particles = new THREE.Points( 
  53.        new THREE.EdgesGeometry(new THREE.CircleGeometry(10, 6)), 
  54.        new THREE.PointsMaterial({ 
  55.            map: textureLoader.load(imageData), 
  56.            color: '#FFF', 
  57.            size: 4, 
  58.            depthWrite: false, 
  59.            transparent: true, 
  60.        }) 
  61.    ); 
  62.    particles.position.z = z; 
  63.    scene.add(particles); 
  64.  
  65.  
  66.    const circle = new THREE.LineSegments( 
  67.        new THREE.EdgesGeometry(new THREE.CircleGeometry(10, 60)), 
  68.        new THREE.LineBasicMaterial({ color: '#FFF' }) 
  69.    ); 
  70.    circle.position.z = z; 
  71.    scene.add(circle); 
  72. } 
  73.  
  74. addCircle('冬至', 4); 
  75. addCircle('夏至', -4); 
2023-03-07 13:01

[ThreeJS] 解決陰影造成的條紋

 會出現條紋現象是因為雙面材質,只要設置為單面材質就可以解決。

  1. /* 載入模型 */ 
  2. new THREE.GLTFLoader().load('Model.glb', function (gltf) { 
  3.    const model = gltf.scene; 
  4.  
  5.    model.traverse(obj => { 
  6.        if (!obj.isMesh) { return; } 
  7.  
  8.        obj.frustumCulled = false; 
  9.        obj.castShadow = true; 
  10.        obj.receiveShadow = true; 
  11.  
  12.        /* 解決陰影造成的條紋 */ 
  13.        obj.material.side = THREE.FrontSide; 
  14.        obj.material.shadowSide = THREE.FrontSide; 
  15.    }); 
  16.  
  17.    scene.add(model); 
  18. }); 
2023-03-07 12:49

[ThreeJS] DragControls 拖移出現異常的原因

 需要進行拖移的模型其[原點]最好設置在[幾何中心]

因為拖移出現異常,我為了解決這個問題,認真看 DragControls 的原始碼,所有處理拖移的邏輯都相當周全,基本上不應該出現異常現象。

在用 console.log 查看拖移的座標數值時,才發現原來是模型的[原點]設置在[場景中心]造成的。

2023-03-07 11:37

[ThreeJS] 載入壓縮過 GLTF 檔

  1. /* 解壓縮 lib 載入器 
  2. * https://threejs.org/docs/#examples/en/loaders/DRACOLoader 
  3. * https://google.github.io/draco/ 
  4. */ 
  5. const dracoLoader = new THREE.DRACOLoader(); 
  6.  
  7. /* 指定解壓器的位置 */ 
  8. dracoLoader.setDecoderPath('../Scripts/three.145/libs/draco/'); 
  9.  
  10.  
  11. /* GLTF 載入器 */ 
  12. const gltfLoader = new THREE.GLTFLoader(); 
  13. gltfLoader.setDRACOLoader(dracoLoader); 
  14.  
  15. /* 載入模型 */ 
  16. gltfLoader.load('Model.glb', function (gltf) { 
  17.    const model = gltf.scene; 
  18.    scene.add(model); 
  19. }); 
2023-03-03 15:08

[ThreeJS] 材質發黑是因為金屬屬性

剛開始接觸 ThreeJS 時,被材質發黑這個問題,困擾很久,一直沒找到問題點,不管怎麼增加 HemisphereLight 都無效,用了四個方向 DirectionalLight 才呈現能接受的效果。

後來才發現問題點是金屬屬性 (metalness),因為金屬有鏡面效果會映像出周圍的景象,然後我又沒有設置 environment map,整個場景 (scene) 都是黑漆漆的,所以材質映像出來也會是黑色的。





2023-02-21 10:33

[Python] Flask Log 配置

  1. import os 
  2. import logging 
  3. import logging.handlers 
  4.  
  5. from flask import Flask, g, request, json 
  6.  
  7. app = Flask(__name__) 
  8.  
  9.  
  10. #[ Log 配置 ]############################################################# 
  11. # 用來記錄無法處理的錯誤 (PS: 使用 WSGI 會依附 Apache 的設定,可以不用配置) 
  12.  
  13. # https://docs.python.org/zh-cn/3/library/logging.html 
  14. formatter = logging.Formatter("%(asctime)s [%(levelname)s]  %(message)s") 
  15.  
  16. # https://docs.python.org/zh-tw/3/library/logging.handlers.html#timedrotatingfilehandler 
  17. handler = logging.handlers.TimedRotatingFileHandler("log/web-api", 
  18.    when = "D", 
  19.    interval = 1, 
  20.    backupCount = 7, 
  21.    encoding = "UTF-8", 
  22.    delay = False, 
  23.    utc = True) 
  24. handler.setFormatter(formatter) 
  25.  
  26. app.logger.addHandler(handler) 
2023-02-21 10:13

[Python] Flask 自訂日期的 Json 轉換

  1. import os 
  2. import datetime 
  3. import time 
  4.  
  5. from flask import Flask, g, request, json 
  6. from flask.json import JSONEncoder 
  7.  
  8.  
  9. class CustomJsonEncoder(JSONEncoder): 
  10.    # 針對日期自訂 Json 轉換 
  11.  
  12.    def default(self, obj): 
  13.        if isinstance(obj, datetime.date): 
  14.            return obj.isoformat().replace('T', ' ') 
  15.  
  16.        return super().default(obj) 
  17.  
  18.  
  19. app = Flask(__name__) 
  20.  
  21. app.json_encoder = CustomJsonEncoder 
  22. app.config['JSON_AS_ASCII'] = False  # 返回結果可以正確顯示中文 
2023-02-21 10:05

[Python] Flask MySQL 連線管理

  1. import os 
  2. import mysql.connector as sql 
  3.  
  4. from werkzeug.exceptions import HTTPException, BadRequest 
  5. from flask import Flask, g, request, json 
  6.  
  7.  
  8. app = Flask(__name__) 
  9.  
  10.  
  11.  
  12. #[ DB 處裡 ]############################################################# 
  13.  
  14. # https://docsxyz.com/zh-hant/wiki/python/connector-python-connectargs 
  15. db_config = { 
  16.    'host'      : "localhost", 
  17.    'user'      : "XXXX", 
  18.    'passwd'    : "XXXX", 
  19.    'db'        : 'XXXX', 
  20.    'use_pure'  : True, 
  21.    'autocommit': True, 
  22.    'charset'   : 'utf8', 
  23. } 
  24.  
  25.  
  26. @app.before_request 
  27. def before_request(): 
  28.    # 在 request 前開啟 DB 連線 
  29.    # g 是 Flask global 在每個 request 有獨立的 context 
  30.  
  31.    g.cnt = sql.connect(**db_config) 
  32.    g.cursor = g.cnt.cursor(dictionary = True) 
  33.  
  34.  
  35.  
  36. @app.after_request 
  37. def after_request(response): 
  38.    # 在 request 後結束 DB 連線 
  39.  
  40.    cursor = g.get('cursor', None) 
  41.    if cursor is not None: 
  42.        # 當 cursor 還有 row 沒有取出,close 會發生錯誤 
  43.        if cursor.with_rows : cursor.fetchall() 
  44.        cursor.close() 
  45.  
  46.    cnt = g.get('cnt', None) 
  47.    if cnt is not None: 
  48.        cnt.close() 
  49.  
  50.    return response 
2023-02-21 09:55

[Python] Flask 筆記

相依套件:
-------------------------------------------------------------------------------
python-3.8.10-amd64.exe

pip install Flask
pip install flask_cors
pip install mysql-connector-python
pip install pycryptodomex
pip install py-linq     # https://viralogic.github.io/py-enumerable/


Apache CGI 配置,用虛擬 Script 指向到 app.cgi
-------------------------------------------------------------------------------


ScriptAlias /web-api  D:/iog-project/web-api/app.cgi

<Directory "D:/iog-project/web-api/">
Options ExecCGI 
AllowOverride all
Require local
</Directory>


-------------------------------------------------------------------------------

# 正常執行
flask run

# 除錯執行  (flask 會提供除錯功能,並將 logger 從 warning 提升到 debug 
export FLASK_ENV=development    # for linux / git  bash 
set FLASK_ENV=development       # for windows cmd
flask run

# 列出設定的路徑
flask routes


其他
-------------------------------------------------------------------------------

因為 web-api 的格式是 Json,所以遵照 JS 的命名風格,欄位名稱開頭小寫第二個字大寫

2023-02-21 09:53

[Python] Flask 錯誤處裡

  1. from werkzeug.exceptions import HTTPException, BadRequest 
  2. from flask import Flask, g, request, json 
  3.  
  4. app = Flask(__name__) 
  5.  
  6.  
  7. #[ 錯誤處裡 ]############################################################# 
  8. @app.errorhandler(Exception) 
  9. def handle_exception(e): 
  10.    if isinstance(e, HTTPException): return e  # 讓 HTTPException 交由下一個處理 
  11.  
  12.    app.logger.exception("Internal Server Error.")  # log 錯誤訊息 
  13.    if app.debug : return e 
  14.  
  15.    return json.jsonify({'message': 'Internal Server Error.'}), 500 
  16.  
  17.  
  18. @app.errorhandler(HTTPException) 
  19. def handle_exception(e): 
  20.    response = e.get_response() 
  21.    return json.jsonify({'message': e.description}), e.code