2026/1/26

ChatTTS

ChatTTS GitHub - 2noise/ChatTTS: A generative speech model for daily dialogue. 是支援中文與英文兩種語言的 TTS engine,特別的是,不僅只是單純的轉換為語音,還能調整音色,設定語速,增加笑聲口頭語等等功能。ChatTTS使用約100,000小時的中文和英文數據進行訓練,開源版本是一個在 40,000 小時語音資料上進行無監督微調的預訓練模型。為了限制 ChatTTS 的使用,在 40,000 小時模型的訓練過程中包含了少量高頻噪音。

文字的部分要注意有些標點符號不支援,另外還有阿拉伯數字也不支援,使用前要先把數字轉換為中文。

安裝

pip3 install torchaudio soundfile
pip3 install git+https://github.com/2noise/ChatTTS

測試

最基本的測試程式

import ChatTTS
import torch
import torchaudio

chat = ChatTTS.Chat()
chat.load(compile=False) # Set to True for better performance

texts = ["In some versions of torchaudio, the first line works but in other versions, so does the second line.", "I needed to install both torchaudio and soundfile in conda isolated environment"]

wavs = chat.infer(texts)

for i in range(len(wavs)):
    """
    In some versions of torchaudio, the first line works but in other versions, so does the second line.
    """
    try:
        torchaudio.save(f"basic_output{i}.wav", torch.from_numpy(wavs[i]).unsqueeze(0), 24000)
    except:
        torchaudio.save(f"basic_output{i}.wav", torch.from_numpy(wavs[i]), 24000)

以下是一個可調整參數的測試程式

import ChatTTS
import torch
import torchaudio

chat = ChatTTS.Chat()
chat.load(compile=False) # Set to True for better performance

# 需要转化为音频的文本内容
text = '重返白宮才要滿三週的川普已經為了削減聯邦支出而發布一系列行政命令。'


###################################
# Sample a speaker from Gaussian.

rand_spk = chat.sample_random_speaker()
print(rand_spk) # save it for later timbre recovery

params_infer_code = ChatTTS.Chat.InferCodeParams(
    spk_emb = rand_spk, # add sampled speaker
    temperature = .3,   # using custom temperature
    top_P = 0.7,        # top P decode
    top_K = 20,         # top K decode
)

###################################
# For sentence level manual control.

# use oral_(0-9), laugh_(0-2), break_(0-7)
# to generate special token in text to synthesize.
params_refine_text = ChatTTS.Chat.RefineTextParams(
    prompt='[oral_2][laugh_0][break_6]',
)

wavs = chat.infer(
    text,
    use_decoder=True,
    params_refine_text=params_refine_text,
    params_infer_code=params_infer_code,
)

for i in range(len(wavs)):
    """
    In some versions of torchaudio, the first line works but in other versions, so does the second line.
    """
    try:
        torchaudio.save(f"output{i}.wav", torch.from_numpy(wavs[i]).unsqueeze(0), 24000)
    except:
        torchaudio.save(f"output{i}.wav", torch.from_numpy(wavs[i]), 24000)

固定音色

程式是以亂數產生一個 speaker。參考# ChatTTS的进阶用法,手把手带你实现个性化配音,音色、语速、停顿,口语 的說明,speaker 是一個 768 維度的向量,所以可以儲存後,然後複製到程式內,就可以固定音色。

例如可用以下的 speaker 向量,產生一個女性音色的語音。

speaker_vector = '-4.741,0.419,-3.355,3.652,-1.682,-1.254,9.719,1.436,0.871,12.334,-0.175,-2.653,-3.132,0.525,1.573,-0.351,0.030,-3.154,0.935,-0.111,-6.306,-1.840,-0.818,9.773,-1.842,-3.433,-6.200,-4.311,1.162,1.023,11.552,2.769,-2.408,-1.494,-1.143,12.412,0.832,-1.203,5.425,-1.481,0.737,-1.487,6.381,5.821,0.599,6.186,5.379,-2.141,0.697,5.005,-4.944,0.840,-4.974,0.531,-0.679,2.237,4.360,0.438,2.029,1.647,-2.247,-1.716,6.338,1.922,0.731,-2.077,0.707,4.959,-1.969,5.641,2.392,-0.953,0.574,1.061,-9.335,0.658,-0.466,4.813,1.383,-0.907,5.417,-7.383,-3.272,-1.727,2.056,1.996,2.313,-0.492,3.373,0.844,-8.175,-0.558,0.735,-0.921,8.387,-7.800,0.775,1.629,-6.029,0.709,-2.767,-0.534,2.035,2.396,2.278,2.584,3.040,-6.845,7.649,-2.812,-1.958,8.794,2.551,3.977,0.076,-2.073,-4.160,0.806,3.798,-1.968,-4.690,5.702,-4.376,-2.396,1.368,-0.707,4.930,6.926,1.655,4.423,-1.482,-3.670,2.988,-3.296,0.767,3.306,1.623,-3.604,-2.182,-1.480,-2.661,-1.515,-2.546,3.455,-3.500,-3.163,-1.376,-12.772,1.931,4.422,6.434,-0.386,-0.704,-2.720,2.177,-0.666,12.417,4.228,0.823,-1.740,1.285,-2.173,-4.285,-6.220,2.479,3.135,-2.790,1.395,0.946,-0.052,9.148,-2.802,-5.604,-1.884,1.796,-0.391,-1.499,0.661,-2.691,0.680,0.848,3.765,0.092,7.978,3.023,2.450,-15.073,5.077,3.269,2.715,-0.862,2.187,13.048,-7.028,-1.602,-6.784,-3.143,-1.703,1.001,-2.883,0.818,-4.012,4.455,-1.545,-14.483,-1.008,-3.995,2.366,3.961,1.254,-0.458,-1.175,2.027,1.830,2.682,0.131,-1.839,-28.123,-1.482,16.475,2.328,-13.377,-0.980,9.557,0.870,-3.266,-3.214,3.577,2.059,1.676,-0.621,-6.370,-2.842,0.054,-0.059,-3.179,3.182,3.411,4.419,-1.688,-0.663,-5.189,-5.542,-1.146,2.676,2.224,-5.519,6.069,24.349,2.509,4.799,0.024,-2.849,-1.192,-16.989,1.845,6.337,-1.936,-0.585,1.691,-3.564,0.931,0.223,4.314,-2.609,0.544,-1.931,3.604,1.248,-0.852,2.991,-1.499,-3.836,1.774,-0.744,0.824,7.597,-1.538,-0.009,0.494,-2.253,-1.293,-0.475,-3.816,8.165,0.285,-3.348,3.599,-4.959,-1.498,-1.492,-0.867,0.421,-2.191,-1.627,6.027,3.667,-21.459,2.594,-2.997,5.076,0.197,-3.305,3.998,1.642,-6.221,3.177,-3.344,5.457,0.671,-2.765,-0.447,1.080,2.504,1.809,1.144,2.752,0.081,-3.700,0.215,-2.199,3.647,1.977,1.326,3.086,34.789,-1.017,-14.257,-3.121,-0.568,-0.316,11.455,0.625,-6.517,-0.244,-8.490,9.220,0.068,-2.253,-1.485,3.372,2.002,-3.357,3.394,1.879,16.467,-2.271,1.377,-0.611,-5.875,1.004,12.487,2.204,0.115,-4.908,-6.992,-1.821,0.211,0.540,1.239,-2.488,-0.411,2.132,2.130,0.984,-10.669,-7.456,0.624,-0.357,7.948,2.150,-2.052,3.772,-4.367,-11.910,-2.094,3.987,-1.565,0.618,1.152,1.308,-0.807,1.212,-4.476,0.024,-6.449,-0.236,5.085,1.265,-0.586,-2.313,3.642,-0.766,3.626,6.524,-1.686,-2.524,-0.985,-6.501,-2.558,0.487,-0.662,-1.734,0.275,-9.230,-3.785,3.031,1.264,15.340,2.094,1.997,0.408,9.130,0.578,-2.239,-1.493,11.034,2.201,6.757,3.432,-4.133,-3.668,2.099,-6.798,-0.102,2.348,6.910,17.910,-0.779,4.389,1.432,-0.649,5.115,-1.064,3.580,4.129,-4.289,-2.387,-0.327,-1.975,-0.892,5.327,-3.908,3.639,-8.247,-1.876,-10.866,2.139,-3.932,-0.031,-1.444,0.567,-5.543,-2.906,1.399,-0.107,-3.044,-4.660,-1.235,-1.011,9.577,2.294,6.615,-1.279,-2.159,-3.050,-6.493,-7.282,-8.546,5.393,2.050,10.068,3.494,8.810,2.820,3.063,0.603,1.965,2.896,-3.049,7.106,-0.224,-1.016,2.531,-0.902,1.436,-1.843,1.129,6.746,-2.184,0.801,-0.965,-7.555,-18.409,6.176,-3.706,2.261,4.158,-0.928,2.164,-3.248,-4.892,-0.008,-0.521,7.931,-10.693,4.320,-0.841,4.446,-1.591,-0.702,4.075,3.323,-3.406,-1.198,-5.518,-0.036,-2.247,-2.638,2.160,-9.644,-3.858,2.402,-2.640,1.683,-0.961,-3.076,0.226,5.106,0.712,0.669,2.539,-4.340,-0.892,0.732,0.775,-2.757,4.365,-2.368,5.368,0.342,-0.655,0.240,0.775,3.686,-4.008,16.296,4.973,1.851,4.747,0.652,-2.117,6.470,2.189,-8.467,3.236,3.745,-1.332,3.583,-2.504,5.596,-2.440,0.995,-2.267,-3.322,3.490,1.156,1.716,0.669,-3.640,-1.709,5.055,6.265,-3.963,2.863,14.129,5.180,-3.590,0.393,0.234,-3.978,6.946,-0.521,1.925,-1.497,-0.283,0.895,-3.969,5.338,-1.808,-3.578,2.699,2.728,-0.895,-2.175,-2.717,2.574,4.571,1.131,2.187,3.620,-0.388,-3.685,0.979,2.731,-2.164,1.628,-1.006,-7.766,-11.033,-10.985,-2.413,-1.967,0.790,0.826,-1.623,-1.783,3.021,1.598,-0.931,-0.605,-1.684,1.408,-2.771,-2.354,5.564,-2.296,-4.774,-2.830,-5.149,2.731,-3.314,-1.002,3.522,3.235,-1.598,1.923,-2.755,-3.900,-3.519,-1.673,-2.049,-10.404,6.773,1.071,0.247,1.120,-0.794,2.187,-0.189,-5.591,4.361,1.772,1.067,1.895,-5.649,0.946,-2.834,-0.082,3.295,-7.659,-0.128,2.077,-1.638,0.301,-0.974,4.331,11.711,4.199,1.545,-3.236,-4.404,-1.333,0.623,1.414,-0.240,-0.816,-0.808,-1.382,0.632,-5.238,0.120,10.634,-2.026,1.702,-0.469,1.252,1.173,3.015,-8.798,1.633,-5.323,2.149,-6.481,11.635,3.072,5.642,5.252,4.702,-3.523,-0.594,4.150,1.392,0.554,-4.377,3.646,-0.884,1.468,0.779,2.372,-0.101,-5.702,0.539,-0.440,5.149,-0.011,-1.899,-1.349,-0.355,0.076,-0.100,-0.004,5.346,6.276,0.966,-3.138,-2.633,-3.124,3.606,-3.793,-3.332,2.359,-0.739,-3.301,-2.775,-0.491,3.283,-1.394,-1.883,1.203,1.097,2.233,2.170,-2.980,-15.800,-6.791,-0.175,-4.600,-3.840,-4.179,6.568,5.935,-0.431,4.623,4.601,-1.726,0.410,2.591,4.016,8.169,1.763,-3.058,-1.340,6.276,4.682,-0.089,1.301,-4.817' # 768维向量
rand_spk = torch.tensor([float(x) for x in speaker_vector.split(',')])

ChatTTS ### 稳定音色查找与音色可查詢並下載已經測試過提供的音色檔案,例如,下載一個音色檔案:seed_2147_restored_emb.pt 後,用以下程式載入該音色檔案,因為是使用 CPU,沒有 GPU,故需要加上 map_location=torch.device('cpu')

rand_spk = torch.load('seed_2147_restored_emb.pt', map_location=torch.device('cpu'))

特效

增加笑聲或停頓,要在文字中間加上

  • [laugh] 代表笑聲

  • [uv_break] 代表停頓

通過 params_refine_text 中的 prompt 參數可以控制笑聲和停頓的強度:

笑聲:laugh_(0-2):laugh_0、laugh_1、laugh_2(笑聲愈加強烈) 停頓:break_(0-7):break_0 至 break_7(停頓逐漸明顯)

實際合成語音時,會自動加上一些停頓,可加上 skip_refine_text=True,強制以原始的方式合成

chat.infer([text], skip_refine_text=True, params_refine_text={"prompt": '[oral_2][laugh_0][break_6]'})

References

ChatTTS: Text-to-Speech For Chat

ChatTTS使用demo示例(包含长文本生成语音、固定音色pt文件)

# ChatTTS使用技巧:如何精细化控制语气、音色、语速 附一键整合包!

## ChatTTS归因分析-音色性别转换方法

# 揭秘ChatTTS:高可控语音合成神器上手实录 带你玩转ChatTTS!

2026/1/19

CRDT

CDRT conflict-free replicated data type 無衝突複製資料類型是一種可以在網路中的多台電腦上複製的資料結構,每一個副本可獨立各自更新,不需要在副本之間進行協調,已透過數學確認可解決可能出現的不一致問題。

對於不同節點上的共用資料,如果在某一個節點上更新了資料,會產生不一致性,如果節點之間沒有協調更新的權限而產生資料衝突,就可能需要放棄一部分更新,以達到最終的一致性,分散式計算都集中在如何防止複製資料的並行更新問題上。

另一種方式 Optimistic Replication,是讓各節點都能做更新,允許所有更新都能同時執行,然後再來考慮如何合併解決衝突問題,CRDT 是一種資料結構,可在不同的副本上進行更新,最後透過合併解決衝突問題。

CRDT 種類

基於狀態的 CRDT 比較容易實作,但需要每個 CRDT 都要將狀態傳給其他副本,傳輸資料耗費較大,基於操作的 CRDT 只需要傳送更新的動作記錄。

基於操作的 CRDT

也稱為 交換性複製資料類型(commutative replicated data types,或CmRDT)

只傳輸更新操作來傳播狀態,必須確保操作過程都有傳遞給每一個資料副本,可不依照順序,但不能重複。

基於狀態的 CRDT

被稱為收斂複製資料類型(convergent replicated data types,或CvRDT)

必須將完整的本地端狀態,重送給其他副本。

State-Based LWW-Element-Graph CRDT

GitHub - juliuskrah/crdt-java: A minimal CRDT implementation 這是一個簡易的 java library,實作了 CvRDT,用 graph 方式,連結不同的節點。

當兩個節點沒有連線時,就是 split-brain 狀態,CRDT 可在節點連接後,讓不同的副本資料合併達到一致性。

這邊測試四種演算法

  • Grow-Only Set:會一直不斷增加的 Set,最終不同節點的 Set 會合併再一起

  • Increment-Only Counter: 結果會是所有節點的加總總和

  • PN Counter:結果是所有節點的 加總總和 - 減法總和

  • Last-Writer-Wins Register:在合併時,只會保留最新的異動內容。同時發生的操作會被丟棄

<!-- Experimental CRDT-implementations for the JVM -->
        <dependency>
            <groupId>com.netopyr.wurmloch</groupId>
            <artifactId>wurmloch-crdt</artifactId>
            <version>0.1.0</version>
        </dependency>
import com.netopyr.wurmloch.crdt.GCounter;
import com.netopyr.wurmloch.crdt.GSet;
import com.netopyr.wurmloch.crdt.LWWRegister;
import com.netopyr.wurmloch.crdt.PNCounter;
import com.netopyr.wurmloch.store.LocalCrdtStore;
import org.junit.Test;

import static org.junit.Assert.*;

public class CRDTTest {
    @Test
    public void gset() {
        LocalCrdtStore crdtStore1 = new LocalCrdtStore();
        LocalCrdtStore crdtStore2 = new LocalCrdtStore();
        crdtStore1.connect(crdtStore2);

        GSet<String> replica1 = crdtStore1.createGSet("fruit");
        GSet<String> replica2 = crdtStore2.<String>findGSet("fruit").get();

        replica1.add("apple");
        replica2.add("banana");

        // 確認replica1, replica2 都有 "apple" 及  “”banana
        assertTrue( replica1.contains("apple") );
        assertTrue( replica1.contains("banana") );
        assertTrue( replica2.contains("apple") );
        assertTrue( replica2.contains("banana") );

        // 刻意斷線
        crdtStore1.disconnect(crdtStore2);
        // 異動 replica1, replica2
        replica1.add("strawberry");
        replica2.add("pear");

        assertTrue( replica1.contains("strawberry") );
        assertFalse( replica2.contains("strawberry") );
        assertFalse( replica1.contains("pear") );
        assertTrue( replica2.contains("pear") );

        // 連線
        crdtStore1.connect(crdtStore2);

        assertTrue( replica1.contains("strawberry") );
        assertTrue( replica2.contains("strawberry") );
        assertTrue( replica1.contains("pear") );
        assertTrue( replica2.contains("pear") );
    }

    @Test
    public void gcounter() {
        // 結果會是所有節點的 sum
        LocalCrdtStore crdtStore1 = new LocalCrdtStore();
        LocalCrdtStore crdtStore2 = new LocalCrdtStore();
        crdtStore1.connect(crdtStore2);

        GCounter replica1 = crdtStore1.createGCounter("counter");
        GCounter replica2 = crdtStore2.findGCounter("counter").get();

        replica1.increment();
        replica2.increment(2L);

        assertEquals(3L, replica1.get());
        assertEquals(3L, replica2.get());

        // 斷線
        crdtStore1.disconnect(crdtStore2);

        replica1.increment(3L);
        replica2.increment(5L);

        assertEquals(6L, replica1.get());
        assertEquals(8L, replica2.get());

        // connect
        crdtStore1.connect(crdtStore2);
        assertEquals(11L, replica1.get());
        assertEquals(11L, replica2.get());
    }

    @Test
    public void pncounter() {
        // 結果是所有節點的 加總總和 - 減法總和
        LocalCrdtStore crdtStore1 = new LocalCrdtStore();
        LocalCrdtStore crdtStore2 = new LocalCrdtStore();
        crdtStore1.connect(crdtStore2);

        PNCounter replica1 = crdtStore1.createPNCounter("pncounter");
        PNCounter replica2 = crdtStore2.findPNCounter("pncounter").get();

        replica1.increment();
        replica2.decrement(2L);

        assertEquals(-1L, replica1.get());
        assertEquals(-1L, replica2.get());

        // disconnect
        crdtStore1.disconnect(crdtStore2);

        replica1.decrement(3L);
        replica2.increment(5L);

        assertEquals(-4L, replica1.get());
        assertEquals(4L, replica2.get());

        // connect
        crdtStore1.connect(crdtStore2);

        assertEquals(1L, replica1.get());
        assertEquals(1L, replica2.get());
    }

    @Test
    public void lwwregister() {
        // 在合併時,只會保留最新的異動內容。同時發生的操作會被丟棄
        LocalCrdtStore crdtStore1 = new LocalCrdtStore("crdt");
        LocalCrdtStore crdtStore2 = new LocalCrdtStore("crdt");
        crdtStore1.connect(crdtStore2);

        LWWRegister<String> replica1 = crdtStore1.createLWWRegister("lwwregister");
        LWWRegister<String> replica2 = crdtStore2.<String>findLWWRegister("lwwregister").get();

        replica1.set("apple");
        replica2.set("banana");

        assertEquals("banana", replica1.get());
        assertEquals("banana", replica2.get());

        //disconnect
        crdtStore1.disconnect(crdtStore2);

        replica1.set("strawberry");
        replica2.set("pear");

        assertEquals("strawberry", replica1.get());
        assertEquals("pear", replica2.get());

        // connect
        crdtStore1.connect(crdtStore2);

        // buggy:應該要是 pear
        assertEquals("strawberry", replica1.get());
        assertEquals("strawberry", replica2.get());
    }
}

References

無衝突複製資料類型 - 維基百科,自由的百科全書

# CRDT — 將非同步資料整合

CRDT - HackMD

Introduction to Conflict-Free Replicated Data Types | Baeldung

GitHub - juliuskrah/crdt-java: A minimal CRDT implementation

2026/1/12

JavaFX 08 webviewJavaFX 08 webview

WebView 是一個可以嵌入網頁瀏覽功能的元件,使用內建的 WebKit 引擎來顯示 HTML 內容(包括 JavaScript、CSS 等)。這對於桌面應用程式需要嵌入網頁內容(如:顯示文件、使用 Web UI 元件)時非常有用。

但該 WebKit engine 不是最新版本,雖然支援 html5/css/javascript,但不支援新的標準,不支援多個分頁,沒有 js console 開發者工具,無法使用 react/vue 這些框架,不能跟 javafx node 直接互動(無法將 node 插入 html DOM)。

webview 裡面的頁面跟 js,可以跟 java code 互動

以下是一個雙向互動的 sample

js 可可

package javafx.webview;

import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.layout.BorderPane;
import javafx.scene.web.WebEngine;
import javafx.scene.web.WebView;
import javafx.stage.Stage;
import netscape.javascript.JSObject;

public class WebViewDemo extends Application {

    @Override
    public void start(Stage primaryStage) {
        WebView webView = new WebView();
        WebEngine webEngine = webView.getEngine();

        // 載入 HTML 字串
        String html = """
            <html>
            <head>
                <script>
                    function callJava() {
                        javaApp.showMessage("Hello from JavaScript!");
                    }

                    function fromJava(data) {
                        document.getElementById("result").innerText = "Java says: " + data;
                    }
                </script>
            </head>
            <body>
                <h2>JavaFX WebView 雙向互動</h2>
                <button onclick="callJava()">呼叫 Java 方法</button>
                <p id="result"></p>
            </body>
            </html>
            """;

        webEngine.loadContent(html);

        // 頁面載入完成後建立 bridge
        webEngine.getLoadWorker().stateProperty().addListener((obs, oldState, newState) -> {
            if (newState == javafx.concurrent.Worker.State.SUCCEEDED) {
                JSObject window = (JSObject) webEngine.executeScript("window");
                window.setMember("javaApp", new JavaBridge(webEngine));
            }
        });

        BorderPane root = new BorderPane(webView);
        Scene scene = new Scene(root, 800, 600);
        primaryStage.setScene(scene);
        primaryStage.setTitle("JavaFX WebView 雙向互動範例");
        primaryStage.show();
    }

    public static void main(String[] args) {
        launch(args);
    }

    // Java Bridge 類別
    public static class JavaBridge {
        private final WebEngine webEngine;

        public JavaBridge(WebEngine webEngine) {
            this.webEngine = webEngine;
        }

        // JS 呼叫這個方法
        public void showMessage(String msg) {
            System.out.println("JavaScript 傳來訊息: " + msg);
            // Java 反呼叫 JS 的 function
            webEngine.executeScript("fromJava('收到: " + msg + "')");
        }
    }
}

2026/1/5

JavaFX 07 chart

用 javafx 繪製圖表的工具

  • 圓餅圖 PieChart 無需軸
  • 折線圖 LineChart<X,Y> 通常 CategoryAxis/NumberAxis
  • 面積圖 AreaChart<X,Y> 同折線圖
  • 長條圖 BarChart<X,Y> 通常 CategoryAxis/NumberAxis
  • 氣泡圖 BubbleChart<Number,Number> 用第三值表示半徑
  • 散點圖 ScatterChart<Number,Number> 點陣圖
  • 累積面積圖 StackedAreaChart<X,Y> 多組堆疊數據區域
  • 累積長條圖 StackedBarChart<X,Y> 長條堆疊比較
package javafx.chart;

import javafx.application.Application;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.chart.*;
import javafx.scene.control.ComboBox;
import javafx.scene.layout.BorderPane;
import javafx.scene.paint.Color;
import javafx.stage.Stage;

//圖表類型    JavaFX 類別名稱    X/Y 軸類型
//圓餅圖    PieChart    無需軸
//折線圖    LineChart<X,Y>    通常 CategoryAxis/NumberAxis
//面積圖    AreaChart<X,Y>    同折線圖
//長條圖    BarChart<X,Y>    通常 CategoryAxis/NumberAxis
//氣泡圖    BubbleChart<Number,Number>    用第三值表示半徑
//散點圖    ScatterChart<Number,Number>    點陣圖
//累積面積圖    StackedAreaChart<X,Y>    多組堆疊數據區域
//累積長條圖    StackedBarChart<X,Y>    長條堆疊比較

public class AllChartsDemo extends Application {

    private BorderPane root;
    private ComboBox<String> chartSelector;

    @Override
    public void start(Stage primaryStage) {
        root = new BorderPane();
        root.setPadding(new Insets(10));

        chartSelector = new ComboBox<>();
        chartSelector.getItems().addAll(
                "PieChart", "LineChart", "AreaChart", "BarChart",
                "BubbleChart", "ScatterChart", "StackedAreaChart", "StackedBarChart"
        );
        chartSelector.setValue("PieChart");
        chartSelector.setOnAction(e -> updateChart(chartSelector.getValue()));

        root.setTop(chartSelector);
        updateChart(chartSelector.getValue());

        Scene scene = new Scene(root, 800, 600);
        primaryStage.setTitle("JavaFX Chart Selector Demo");
        primaryStage.setScene(scene);
        primaryStage.show();
    }

    private void updateChart(String type) {
        switch (type) {
            case "PieChart":
                PieChart pieChart = new PieChart();
                pieChart.setTitle("Pie Chart");
                pieChart.getData().addAll(
                        new PieChart.Data("Java", 30),
                        new PieChart.Data("Python", 25),
                        new PieChart.Data("C++", 20),
                        new PieChart.Data("Kotlin", 15),
                        new PieChart.Data("Other", 10)
                );
                root.setCenter(pieChart);
                break;

            case "LineChart":
                CategoryAxis x1 = new CategoryAxis();
                NumberAxis y1 = new NumberAxis();
                LineChart<String, Number> lineChart = new LineChart<>(x1, y1);
                lineChart.setTitle("Line Chart");
                XYChart.Series<String, Number> lineSeries = new XYChart.Series<>();
                lineSeries.setName("Sales");
                lineSeries.getData().add(new XYChart.Data<>("Mon", 10));
                lineSeries.getData().add(new XYChart.Data<>("Tue", 20));
                lineSeries.getData().add(new XYChart.Data<>("Wed", 15));
                lineSeries.getData().add(new XYChart.Data<>("Thu", 25));
                lineSeries.getData().add(new XYChart.Data<>("Fri", 18));
                lineChart.getData().add(lineSeries);
                root.setCenter(lineChart);
                break;

            case "AreaChart":
                AreaChart<String, Number> areaChart = new AreaChart<>(new CategoryAxis(), new NumberAxis());
                areaChart.setTitle("Area Chart");
                XYChart.Series<String, Number> areaSeries = new XYChart.Series<>();
                areaSeries.setName("Traffic");
                areaSeries.getData().add(new XYChart.Data<>("Jan", 200));
                areaSeries.getData().add(new XYChart.Data<>("Feb", 300));
                areaSeries.getData().add(new XYChart.Data<>("Mar", 150));
                areaSeries.getData().add(new XYChart.Data<>("Apr", 250));
                areaChart.getData().add(areaSeries);
                root.setCenter(areaChart);
                break;

            case "BarChart":
                BarChart<String, Number> barChart = new BarChart<>(new CategoryAxis(), new NumberAxis());
                barChart.setTitle("Bar Chart");
                XYChart.Series<String, Number> barSeries = new XYChart.Series<>();
                barSeries.setName("Downloads");
                barSeries.getData().add(new XYChart.Data<>("Chrome", 60));
                barSeries.getData().add(new XYChart.Data<>("Firefox", 40));
                barSeries.getData().add(new XYChart.Data<>("Edge", 30));
                barChart.getData().add(barSeries);
                root.setCenter(barChart);
                break;

            case "BubbleChart":
                BubbleChart<Number, Number> bubbleChart = new BubbleChart<>(new NumberAxis(), new NumberAxis());
                bubbleChart.setTitle("Bubble Chart");
                XYChart.Series<Number, Number> bubbleSeries = new XYChart.Series<>();
                bubbleSeries.setName("Bubbles");
                bubbleSeries.getData().add(new XYChart.Data<>(10, 20, 5));
                bubbleSeries.getData().add(new XYChart.Data<>(15, 30, 10));
                bubbleSeries.getData().add(new XYChart.Data<>(25, 10, 7));
                bubbleChart.getData().add(bubbleSeries);
                root.setCenter(bubbleChart);
                break;

            case "ScatterChart":
                ScatterChart<Number, Number> scatterChart = new ScatterChart<>(new NumberAxis(), new NumberAxis());
                scatterChart.setTitle("Scatter Chart");
                XYChart.Series<Number, Number> scatterSeries = new XYChart.Series<>();
                scatterSeries.setName("Points");
                scatterSeries.getData().add(new XYChart.Data<>(5, 20));
                scatterSeries.getData().add(new XYChart.Data<>(10, 40));
                scatterSeries.getData().add(new XYChart.Data<>(15, 25));
                scatterChart.getData().add(scatterSeries);
                root.setCenter(scatterChart);
                break;

            case "StackedAreaChart":
                StackedAreaChart<String, Number> stackedAreaChart = new StackedAreaChart<>(new CategoryAxis(), new NumberAxis());
                stackedAreaChart.setTitle("Stacked Area Chart");
                XYChart.Series<String, Number> sa1 = new XYChart.Series<>();
                sa1.setName("Team A");
                sa1.getData().add(new XYChart.Data<>("Q1", 100));
                sa1.getData().add(new XYChart.Data<>("Q2", 120));
                sa1.getData().add(new XYChart.Data<>("Q3", 90));
                sa1.getData().add(new XYChart.Data<>("Q4", 110));
                XYChart.Series<String, Number> sa2 = new XYChart.Series<>();
                sa2.setName("Team B");
                sa2.getData().add(new XYChart.Data<>("Q1", 80));
                sa2.getData().add(new XYChart.Data<>("Q2", 95));
                sa2.getData().add(new XYChart.Data<>("Q3", 70));
                sa2.getData().add(new XYChart.Data<>("Q4", 85));
                stackedAreaChart.getData().addAll(sa1, sa2);
                root.setCenter(stackedAreaChart);
                break;

            case "StackedBarChart":
                StackedBarChart<String, Number> stackedBarChart = new StackedBarChart<>(new CategoryAxis(), new NumberAxis());
                stackedBarChart.setTitle("Stacked Bar Chart");
                XYChart.Series<String, Number> sb1 = new XYChart.Series<>();
                sb1.setName("Male");
                sb1.getData().add(new XYChart.Data<>("2021", 60));
                sb1.getData().add(new XYChart.Data<>("2022", 70));
                XYChart.Series<String, Number> sb2 = new XYChart.Series<>();
                sb2.setName("Female");
                sb2.getData().add(new XYChart.Data<>("2021", 40));
                sb2.getData().add(new XYChart.Data<>("2022", 55));
                stackedBarChart.getData().addAll(sb1, sb2);
                root.setCenter(stackedBarChart);
                break;
        }
    }

    public static void main(String[] args) {
        launch(args);
    }
}

2025/12/29

JavaFX 06 tranformation, transition

javafx 可套用 transformation 在單一或一組 nodes,也可以針對某一個 node,同時套用多個 transformations。

transformation

transformation 有以下這幾種

  • Rotation:旋轉
  • Scaling:縮放
  • Translation:平移
  • Shearing(斜切):用仿射變換實現
package javafx.transformation;

import javafx.application.Application;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.layout.VBox;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.scene.transform.*;
import javafx.stage.Stage;

//Rotation:旋轉
//Scaling:縮放
//Translation:平移
//Shearing(斜切):用仿射變換實現
public class TransformDemo extends Application {

    @Override
    public void start(Stage primaryStage) {
        // === 建立形狀 ===
        Rectangle rect = new Rectangle(100, 60, Color.CORNFLOWERBLUE);
        rect.setArcWidth(15);
        rect.setArcHeight(15);

        // === 各種變換按鈕 ===
        Button btnTranslate = new Button("Translate (+100, +50)");
        btnTranslate.setOnAction(e -> {
            rect.getTransforms().add(new Translate(100, 50));
        });

        Button btnRotate = new Button("Rotate 45°");
        btnRotate.setOnAction(e -> {
            rect.getTransforms().add(new Rotate(45, rect.getX() + rect.getWidth() / 2, rect.getY() + rect.getHeight() / 2));
        });

        Button btnScale = new Button("Scale x1.5");
        btnScale.setOnAction(e -> {
            rect.getTransforms().add(new Scale(1.5, 1.5, rect.getX() + rect.getWidth() / 2, rect.getY() + rect.getHeight() / 2));
        });

        Button btnShear = new Button("Shear x=0.5, y=0");
        btnShear.setOnAction(e -> {
            rect.getTransforms().add(new Shear(0.5, 0));
        });

        // === Reset 按鈕 ===
        Button btnReset = new Button("Reset Transforms");
        btnReset.setStyle("-fx-background-color: tomato; -fx-text-fill: white;");
        btnReset.setOnAction(e -> {
            rect.getTransforms().clear(); // 移除所有變換
        });

        // === 佈局 ===
        VBox root = new VBox(15,
                rect,
                btnTranslate,
                btnRotate,
                btnScale,
                btnShear,
                btnReset
        );
        root.setPadding(new Insets(20));

        Scene scene = new Scene(root, 500, 400);
        primaryStage.setTitle("JavaFX Transformations with Reset");
        primaryStage.setScene(scene);
        primaryStage.show();
    }

    public static void main(String[] args) {
        launch(args);
    }
}

transition

transform 是一種即時的直接的轉換,會直接改變 node 的屬性,立刻生效。用作 node 的幾何變換。

transition 是一種動畫,需要一段時間,才會轉換到結果的動畫,可以 delay, repeat,有移動、縮放、旋轉、淡入淡出、路徑移動、視覺強化這些動態轉換的效果。

package javafx.transition;

import javafx.animation.*;
import javafx.application.Application;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.layout.VBox;
import javafx.scene.paint.Color;
import javafx.scene.shape.*;
import javafx.stage.Stage;
import javafx.util.Duration;

public class AllTransitionsDemo extends Application {

    private Rectangle rect;

    // 紀錄狀態
    private double currentTranslateX = 0;
    private double currentTranslateY = 0;
    private double currentRotate = 0;
    private double currentScaleX = 1;
    private double currentScaleY = 1;

    @Override
    public void start(Stage primaryStage) {
        rect = new Rectangle(100, 60, Color.CORNFLOWERBLUE);
        rect.setArcWidth(15);
        rect.setArcHeight(15);
        rect.setStroke(Color.DARKBLUE);
        rect.setStrokeWidth(3);

        // RotateTransition
        Button btnRotate = new Button("Rotate +45°");
        btnRotate.setOnAction(e -> {
            currentRotate += 45;
            RotateTransition rt = new RotateTransition(Duration.seconds(1), rect);
            rt.setToAngle(currentRotate);
            rt.play();
        });

        // ScaleTransition
        Button btnScale = new Button("Scale x1.5");
        btnScale.setOnAction(e -> {
            currentScaleX *= 1.5;
            currentScaleY *= 1.5;
            ScaleTransition st = new ScaleTransition(Duration.seconds(1), rect);
            st.setToX(currentScaleX);
            st.setToY(currentScaleY);
            st.play();
        });

        // TranslateTransition
        Button btnTranslate = new Button("Translate (+100, +50)");
        btnTranslate.setOnAction(e -> {
            currentTranslateX += 100;
            currentTranslateY += 50;
            TranslateTransition tt = new TranslateTransition(Duration.seconds(1), rect);
            tt.setToX(currentTranslateX);
            tt.setToY(currentTranslateY);
            tt.play();
        });

        // FadeTransition
        Button btnFade = new Button("Fade Out / In");
        btnFade.setOnAction(e -> {
            FadeTransition ft = new FadeTransition(Duration.seconds(1), rect);
            ft.setFromValue(1.0);
            ft.setToValue(0.2);
            ft.setAutoReverse(true);
            ft.setCycleCount(2);
            ft.play();
        });

        // FillTransition (填充色)
        Button btnFill = new Button("Fill Color Change");
        btnFill.setOnAction(e -> {
            FillTransition fillTransition = new FillTransition(Duration.seconds(2), rect);
            fillTransition.setFromValue((Color) rect.getFill());
            fillTransition.setToValue(Color.ORANGERED);
            fillTransition.setAutoReverse(true);
            fillTransition.setCycleCount(2);
            fillTransition.play();
        });

        // StrokeTransition (邊框色)
        Button btnStroke = new Button("Stroke Color Change");
        btnStroke.setOnAction(e -> {
            StrokeTransition strokeTransition = new StrokeTransition(Duration.seconds(2), rect);
            strokeTransition.setFromValue((Color) rect.getStroke());
            strokeTransition.setToValue(Color.GREEN);
            strokeTransition.setAutoReverse(true);
            strokeTransition.setCycleCount(2);
            strokeTransition.play();
        });

        // SequentialTransition
        Button btnSequential = new Button("Sequential Animation");
        btnSequential.setOnAction(e -> {
            RotateTransition rotate = new RotateTransition(Duration.seconds(1), rect);
            rotate.setByAngle(90);

            TranslateTransition translate = new TranslateTransition(Duration.seconds(1), rect);
            translate.setByX(100);
            translate.setByY(50);

            ScaleTransition scale = new ScaleTransition(Duration.seconds(1), rect);
            scale.setToX(1);
            scale.setToY(1);

            SequentialTransition seq = new SequentialTransition(rect, rotate, translate, scale);
            seq.play();
        });

        // ParallelTransition
        Button btnParallel = new Button("Parallel Animation");
        btnParallel.setOnAction(e -> {
            RotateTransition rotate = new RotateTransition(Duration.seconds(2), rect);
            rotate.setByAngle(180);

            FadeTransition fade = new FadeTransition(Duration.seconds(2), rect);
            fade.setFromValue(1);
            fade.setToValue(0.3);

            ParallelTransition parallel = new ParallelTransition(rect, rotate, fade);
            parallel.play();
        });

        // PauseTransition
        Button btnPause = new Button("Pause 2 Seconds");
        btnPause.setOnAction(e -> {
            PauseTransition pause = new PauseTransition(Duration.seconds(2));
            pause.setOnFinished(ev -> System.out.println("Pause finished"));
            pause.play();
        });

        // PathTransition
        Button btnPath = new Button("Path Animation");
        btnPath.setOnAction(e -> {
            Path path = new Path();
            path.getElements().add(new MoveTo(0, 0));
            path.getElements().add(new CubicCurveTo(150, 100, 300, 0, 400, 100));

            PathTransition pathTransition = new PathTransition(Duration.seconds(3), path, rect);
            pathTransition.setCycleCount(1);
            pathTransition.play();
        });

        // Reset 按鈕
        Button btnReset = new Button("Reset All");
        btnReset.setStyle("-fx-background-color: tomato; -fx-text-fill: white;");
        btnReset.setOnAction(e -> {
            rect.getTransforms().clear();
            rect.setTranslateX(0);
            rect.setTranslateY(0);
            rect.setRotate(0);
            rect.setScaleX(1);
            rect.setScaleY(1);
            rect.setOpacity(1);
            rect.setFill(Color.CORNFLOWERBLUE);
            rect.setStroke(Color.DARKBLUE);

            currentTranslateX = 0;
            currentTranslateY = 0;
            currentRotate = 0;
            currentScaleX = 1;
            currentScaleY = 1;
        });

        VBox root = new VBox(10,
                rect,
                btnRotate,
                btnScale,
                btnTranslate,
                btnFade,
                btnFill,
                btnStroke,
                btnSequential,
                btnParallel,
                btnPause,
                btnPath,
                btnReset
        );
        root.setPadding(new Insets(15));

        Scene scene = new Scene(root, 600, 700);
        primaryStage.setTitle("JavaFX All Transitions Demo");
        primaryStage.setScene(scene);
        primaryStage.show();
    }

    public static void main(String[] args) {
        launch(args);
    }
}