用 p5.js 實現(xiàn)圖片組成的文字效果(含2D和3D版本的代碼)

本文代碼見:https://github.com/DesertsX/p5js-zju126

又是一年浙大校慶時

第一篇 Three.js Shader 教程總算本周上周更新了骇塘,想著在更新這個系列的同時穿插寫些簡單輕松的內容蝎宇,填些之前留下的小坑瑞筐。

今天昨天前天是2023年5月21號珍剑,是浙大126周年校慶的日子笨蚁。想到去年125周年校慶官方制作了個可以輸入校友專業(yè)和名字么抗、認證通過后會生成每個人專屬的由“燦若繁星”的浙大人姓名的粒子系統(tǒng)組成的“求是星海”的網(wǎng)頁矮固。(網(wǎng)頁還在,但需要輸入信息才能生成譬淳,所以大家看不到档址,這里錄個視頻供大家觀賞下,可參見原文里的視頻內容:「用 p5.js 實現(xiàn)圖片組成的文字效果(含2D和3D版本的代碼) - 牛衣古柳」

當時古柳剛接觸 Three.js Shader 沒多久邻梆,看到這個作品后就用 Yuri 油管視頻里提到的 Spector.js 插件看了下該網(wǎng)頁的 Shader 代碼守伸,見到熟悉的 Vertex Shader 頂點著色器、熟悉的 Fragment Shader 片元著色器浦妄,果然就是粒子系統(tǒng)尼摹。

記得那會古柳一方面感慨接觸 Shader 后,“孕婦效應”再次顯現(xiàn)剂娄,到處都能看到 Shader 的應用蠢涝,這次校慶里居然就有“送上門”的;一方面覺得這些人怎么都會 Shader阅懦,仿佛就自己沒學會和二,非常沮喪。

不過說起來去年比較特別耳胎,印象里前些年以及今年都沒有類似專門做個網(wǎng)頁效果的惯吕,一般都是文章或視頻的形式惕它。

帝國大廈亮燈祝賀

之所以提這件事,是古柳想到更早之前2017年120周年校慶那時废登,本來碰上這種滿十年的年份就感覺非常特殊怠缸、更為熱鬧,而當時更有美國紐約帝國大廈專門為浙大120周年校慶亮燈這則“大新聞”钳宪,說可能是帝國大廈有史以來第一次為中國國內大學校慶而亮燈揭北,就很有排面。

畢竟素來幾所985高校間喜歡爭論誰才是中國內地Top3的大學吏颖,并且各自喜歡用各種排行榜里自己的好名次給自己臉上“貼金”搔体。難得讓我等浙大師生或校友遇上這么個“有史以來第一次為中國國內大學校慶而亮燈”的“大噱頭”,可不得“奔走相告”半醉。

而那會剛自學 Python 編程和爬蟲沒多久的古柳也想做點什么應個景疚俱、湊個熱鬧,于是想到可以爬取帝國大廈官網(wǎng)的過往亮燈圖片來拼個 "ZJU 120" 的字樣缩多。

說干就干呆奕,當時帝國大廈的官網(wǎng)可以選擇日期以顯示當天的亮燈圖片,直接遍歷日期用 post 請求就能爬取圖片衬吆。原本古柳以為是每天專門拍攝的照片梁钾,后來發(fā)現(xiàn)沒爬多少就重復了,其實沒多少不同的圖逊抡。

有了圖片后姆泻,古柳在隨手拿的餐巾紙上畫了下“ZJU 120”字樣以確定應該如何布局。

然后古柳用 Python 里的 PIL 庫手動將每張圖片放到文字的位置上冒嫡,當時用的還是笨辦法手動存了每個位置坐標拇勃,但能實現(xiàn)出來就行,本文將介紹個簡單直觀的方法孝凌。

最后分別用單張圖片和不同圖片排列出文字方咆,效果對當時的自己來說還是很酷的。

p5.js 復現(xiàn)上述效果

交代完上述前因后果蟀架,其實本文的目的就是用 p5.js 復現(xiàn)下當初的圖片拼湊出文字的效果瓣赂。

雖然之前古柳幾乎僅在「伴隨 P5.js 入坑創(chuàng)意編程 - 牛衣古柳 - 2019.06.28」一文提到 p5.js,但這個庫真的很簡單辜窑,即便是藝術家钩述、設計師、編程小白都能輕松上手穆碎,而且拿來做創(chuàng)意編程、生成藝術职恳、NFT 作品所禀、數(shù)據(jù)可視化方面、WebGL 3D、Shader 編程色徘、AR/VR/XR 等都可以恭金。

p5.js 有多簡單,讓我們一起看看褂策。首先引入 p5.js 的庫横腿;接著一般需要在 setup() 函數(shù)和 draw() 函數(shù)里書寫代碼,兩者都會被 p5.js 自動執(zhí)行斤寂,前者只執(zhí)行一次耿焊,可以初始設置一些內容,比如設置畫布大小遍搞,后者會被反復調用執(zhí)行罗侯,比如一般電腦60FPS幀率就是每秒執(zhí)行60次,可以實現(xiàn)動畫溪猿、交互钩杰。因為這里僅靜態(tài)地展示圖片無需動態(tài)效果,所以只需在 setup() 里實現(xiàn)即可诊县。

下面我們在 400x400 淺綠色背景的畫布上繪制一個深綠色填充且無描邊的矩形讲弄,其位置在(50, 50)處、寬高為(100,100)依痊,可以看到代碼非常的直觀垂睬,簡直和用 PS/AI 等軟件工具直接畫一個矩形一樣簡單。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>p5.js 實現(xiàn)圖片組成的文字效果</title>
    <style>
        body {
            margin: 0;
        }
    </style>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.6.0/p5.min.js"></script>
</head>

<body>
    <script>
        function setup() {
            createCanvas(400, 400);
            background('#e6fcf5');

            noStroke();
            fill('#0ca678');
            rect(50, 50, 100, 100);
        }
    </script>
</body>
</html>

借助二維數(shù)據(jù)顯示特定形狀

繪制出一個矩形后抗悍,我們如何將一堆矩形繪制出特定的形狀或文字驹饺?這里介紹下古柳之前學 Canvas 時從 Coding Math 這個系列教程里學到的一招,即在二維數(shù)組直接存儲所需形狀格式的數(shù)據(jù)缴渊。

比如同樣的 400x400 畫布赏壹,假如我們將其劃分成5x5的網(wǎng)格,一個單元格就是80x80衔沼,那么我們可以直接在5x5的二維數(shù)組里以下面的格式將我們想在哪些位置繪制矩形用不同數(shù)字進行區(qū)分蝌借,1就是繪制,0就是不繪制指蚁,這樣這個數(shù)組就和實際想繪制的形狀非常直觀的對應上菩佑,比如下圖繪制的“X”形狀。

注意這里矩形寬高 (width / row.length, y * height / grid.length) 就是80x80凝化,直接固定寫死也問題不大稍坯,width 和 height 是創(chuàng)建畫布后就能拿到的畫布寬高,分別除以列數(shù)行數(shù)就是每個單元格的大小。

const grid = [
    [1, 0, 0, 0, 1],
    [0, 1, 0, 1, 0],
    [0, 0, 1, 0, 0],
    [0, 1, 0, 1, 0],
    [1, 0, 0, 0, 1],
];

function setup() {
    createCanvas(400, 400);
    background('#e6fcf5');

    noStroke();
    fill('#0ca678');
    // rect(50, 50, 100, 100);
    for (let y = 0; y < grid.length; y++) {
        const row = grid[y]
        for (let x = 0; x < row.length; x++) {
            if (row[x] === 1) {
                rect(x * width / row.length, y * height / grid.length, width / row.length, height / grid.length);
            }
        }
    }
}

在 grid 里調整形狀非常方便瞧哟,比如很簡單就能變成“回”字形狀混巧,再也不用很笨的去數(shù)每個位置具體的坐標然后手動繪制。

const grid = [
    [1, 1, 1, 1, 1],
    [1, 0, 0, 0, 1],
    [1, 0, 1, 0, 1],
    [1, 0, 0, 0, 1],
    [1, 1, 1, 1, 1],
];

用圖片替換矩形

接著我們用圖片替換矩形勤揩,在 p5.js 的 setup() 函數(shù)之前的 preload() 函數(shù)里通過 loadImage() 方法加載圖片咧党,然后在 setup() 里通過 image() 方法將圖片在對應位置以特定寬高放置即可,這里 image() 后四個參數(shù)和 rect() 一樣陨亡,且因為用的就是 1:1 的圖片所以直接替換即可傍衡。

const grid = [
    [1, 0, 0, 0, 1],
    [0, 1, 0, 1, 0],
    [0, 0, 1, 0, 0],
    [0, 1, 0, 1, 0],
    [1, 0, 0, 0, 1],
];

let img;

function preload() {
    img = loadImage('./images/0.jpeg');
}

function setup() {
    createCanvas(400, 400);
    background('#e6fcf5');

    noStroke();
    fill('#0ca678');
    for (let y = 0; y < grid.length; y++) {
        const row = grid[y]
        for (let x = 0; x < row.length; x++) {
            if (row[x] === 1) {
                // rect(x * width / row.length, y * height / grid.length, width / row.length, height / grid.length);
                image(img, x * width / row.length, y * height / grid.length, width / row.length, height / grid.length);
            }
        }
    }
}

如果需要加載多張圖片,只需要放進數(shù)組里负蠕,使用時按索引獲取對應圖片即可蛙埂。

// let img;
const num = 9;
const images = [];

function preload() {
    // img = loadImage('./images/0.jpeg');
    for (let i = 1; i <= num; i++) {
        const img = loadImage(`./images/${i}.jpeg`)
        images.push(img);
    }
}

最終的“ZJU 126”效果

最后,只需套到實際圖片數(shù)據(jù)集上和基于想要的文字效果去設置 grid 即可虐急。這里因為帝國大廈官網(wǎng)的圖片寬高不一箱残,而本文只是演示如何復現(xiàn),就不去手動剪裁圖片了止吁,有些拉伸變形無關緊要被辑,簡單設置繪制的圖片高度 h 為寬度 w 的1.5倍。然后設置 grid 成 “ZJU 126” 的字樣敬惦,行列數(shù) rows cols 隨之確定盼理,然后畫布寬高大小也能確定,最后就是遍歷繪制9張亮燈圖里的任意一張即可俄删。

const num = 9;
const images = [];
const w = 40;
const h = w * 1.5;

const grid = [
    [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
    [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
    // ZJU
    [0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 1, 0, 0],
    [0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 1, 0, 0],
    [0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 1, 0, 0],
    [0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 1, 0, 0],
    [0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 1, 0, 0],
    [0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 0, 0],
    // 
    [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
    [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
    // 126
    [0, 0, 0, 0, 0, 1, 0, 0, 1, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0],
    [0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0],
    [0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0],
    [0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0],
    [0, 0, 0, 0, 0, 1, 0, 0, 1, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0],
    // 
    [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
    [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
];
const rows = grid.length;
const cols = grid[0].length;

function preload() {
    for (let i = 1; i <= num; i++) {
        const img = loadImage(`./images/${i}.jpeg`);
        images.push(img);
    }
}

function setup() {
    createCanvas(cols * w, rows * h);
    background(10);

    // let i = 0;
    for (let y = 0; y < rows; y++) {
        const row = grid[y];
        for (let x = 0; x < cols; x++) {
            if (row[x] === 1) {
                // const img = images[i % images.length];
                const img = random(images);
                image(img, x * w, y * h, w, h);
                // i++;
            }
        }
    }
}

3D 效果同樣可以

以上的實現(xiàn)宏怔,是不是很簡單,而且用 grid 來擺放圖形元素如此方便畴椰,古柳想到同樣可以在 3D 里擺出立體字的效果臊诊,之所以有這個想法,是因為一直記得五十嵐威暢的這張海報斜脂,覺得蠻漂亮的抓艳。

因此古柳簡單地切換到 p5.js 的 WEBGL 模式,然后在每個位置放上 box() 立方體帚戳,并將圖片貼上去玷或,在正交相機下做出2.5D效果如圖所示。大家覺得是上面2D的好看還是這個3D的好看呢片任?

這部分代碼就不做解釋了偏友,3D WEBGL 的東西解釋起來也麻煩些,大家可自行學習对供,此處僅供參考位他。

const num = 9;
const images = [];
const w = 40;
const h = w * 1.5;

const grid = [
    [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
    [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
    // ZJU
    [0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 1, 0, 0],
    [0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 1, 0, 0],
    [0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 1, 0, 0],
    [0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 1, 0, 0],
    [0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 1, 0, 0],
    [0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 0, 0],
    // 
    [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
    [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
    // 126
    [0, 0, 0, 0, 0, 1, 0, 0, 1, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0],
    [0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0],
    [0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0],
    [0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0],
    [0, 0, 0, 0, 0, 1, 0, 0, 1, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0],
    // 
    [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
    [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
];
const rows = grid.length;
const cols = grid[0].length;

function preload() {
    for (let i = 1; i <= num; i++) {
        const img = loadImage(`./images/${i}.jpeg`);
        images.push(img);
    }
}

function setup() {
    createCanvas(cols * w, rows * h, WEBGL);
    background(10);
    // background('#e6fcf5');

    // camera 位置、朝向、自身的法向量方向棱诱,需要3個向量泼橘、9個坐標來確定
    camera(200, -400, height / 2 / tan(PI / 6), 0, 0, 0, 0, 1, 0);
    ortho(-500, 500, -500, 500, 0.1, 2000);

    let i = 0
    for (let y = 0; y < rows; y++) {
        const row = grid[y];
        for (let x = 0; x < cols; x++) {
            if (row[x] === 1) {
                push();
                noStroke();
                translate((x - cols / 2) * w, (y - rows / 2) * h, -10);
                // const img = random(images);
                const img = images[i % images.length];
                texture(img);
                box(w, h, h);
                pop();
                i++;
            }
        }
    }
}

照例

最后和古柳交流涝动,也請多點贊支持迈勋!

?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市醋粟,隨后出現(xiàn)的幾起案子靡菇,更是在濱河造成了極大的恐慌,老刑警劉巖米愿,帶你破解...
    沈念sama閱讀 218,682評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件厦凤,死亡現(xiàn)場離奇詭異,居然都是意外死亡育苟,警方通過查閱死者的電腦和手機较鼓,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,277評論 3 395
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來违柏,“玉大人博烂,你說我怎么就攤上這事∈” “怎么了禽篱?”我有些...
    開封第一講書人閱讀 165,083評論 0 355
  • 文/不壞的土叔 我叫張陵,是天一觀的道長馍惹。 經(jīng)常有香客問我躺率,道長,這世上最難降的妖魔是什么万矾? 我笑而不...
    開封第一講書人閱讀 58,763評論 1 295
  • 正文 為了忘掉前任悼吱,我火速辦了婚禮,結果婚禮上良狈,老公的妹妹穿的比我還像新娘后添。我一直安慰自己,他們只是感情好们颜,可當我...
    茶點故事閱讀 67,785評論 6 392
  • 文/花漫 我一把揭開白布吕朵。 她就那樣靜靜地躺著,像睡著了一般窥突。 火紅的嫁衣襯著肌膚如雪努溃。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,624評論 1 305
  • 那天阻问,我揣著相機與錄音梧税,去河邊找鬼。 笑死第队,一個胖子當著我的面吹牛哮塞,可吹牛的內容都是我干的。 我是一名探鬼主播凳谦,決...
    沈念sama閱讀 40,358評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼忆畅,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了尸执?” 一聲冷哼從身側響起家凯,我...
    開封第一講書人閱讀 39,261評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎如失,沒想到半個月后绊诲,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,722評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡褪贵,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,900評論 3 336
  • 正文 我和宋清朗相戀三年掂之,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片脆丁。...
    茶點故事閱讀 40,030評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡世舰,死狀恐怖,靈堂內的尸體忽然破棺而出偎快,到底是詐尸還是另有隱情冯乘,我是刑警寧澤,帶...
    沈念sama閱讀 35,737評論 5 346
  • 正文 年R本政府宣布晒夹,位于F島的核電站裆馒,受9級特大地震影響,放射性物質發(fā)生泄漏丐怯。R本人自食惡果不足惜喷好,卻給世界環(huán)境...
    茶點故事閱讀 41,360評論 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望读跷。 院中可真熱鬧梗搅,春花似錦、人聲如沸效览。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,941評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽丐枉。三九已至哆键,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間瘦锹,已是汗流浹背籍嘹。 一陣腳步聲響...
    開封第一講書人閱讀 33,057評論 1 270
  • 我被黑心中介騙來泰國打工闪盔, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人辱士。 一個月前我還...
    沈念sama閱讀 48,237評論 3 371
  • 正文 我出身青樓泪掀,卻偏偏與公主長得像,于是被迫代替她去往敵國和親颂碘。 傳聞我的和親對象是個殘疾皇子异赫,可洞房花燭夜當晚...
    茶點故事閱讀 44,976評論 2 355

推薦閱讀更多精彩內容