ImageGradientComponent 图片渐变遮罩组件

简介

ImageGradientComponent 是一个 LayaAir 组件,用于为图片和文本添加渐变遮罩效果。支持两种模式:

  • 遮罩图模式:使用灰度遮罩图控制渐变的应用范围和强度
  • 代码渐变模式:无遮罩图时,使用代码生成垂直渐变

参数说明

基础参数

参数名 类型 默认值 说明
maskTexture Texture null 遮罩图(灰度图),可在 IDE 中拖拽资源。为空时使用代码渐变
topColor Color [1, 0.42, 0.21, 1] 顶部渐变色 (RGBA,IDE中为0-1范围)
bottomColor Color [0, 0.31, 0.54, 1] 底部渐变色 (RGBA,IDE中为0-1范围)
strength Float 0.8 渐变强度 (0=无效果, 1=完全应用渐变)
tile Boolean false 遮罩图是否平铺 (false=拉伸填充, true=平铺重复)
autoApply Boolean true 是否自动应用渐变

快速开始

方式一:在 IDE 中使用

  1. 选中 Image、Text 节点
  2. 添加组件 → ImageGradientComponent
  3. 在属性面板中配置参数:
    • 拖拽遮罩图到 maskTexture(可选)
    • 设置 topColorbottomColor
    • 调整 strength 强度

方式二:代码中使用(有遮罩图)

import { ImageGradientComponent } from "./ImageGradientComponent";

// 为 Image 添加渐变
const img = new Laya.Image();
img.skin = "resources/image.png";
Laya.stage.addChild(img);

// 加载遮罩图
Laya.loader.load("mask.png", Laya.Handler.create(this, (maskTex: Laya.Texture) => {
    const gradient = img.addComponent(ImageGradientComponent);
    gradient.maskTexture = maskTex;
    gradient.topColor = { r: 1, g: 0.42, b: 0.21, a: 1 };  // 橙色
    gradient.bottomColor = { r: 0, g: 0.31, b: 0.54, a: 1 }; // 深蓝色
    gradient.strength = 0.8;
    gradient.tile = false;  // 拉伸填充
}));

方式三:代码渐变(无遮罩图)

import { ImageGradientComponent } from "./ImageGradientComponent";

const img = new Laya.Image();
img.skin = "resources/image.png";
Laya.stage.addChild(img);

const gradient = img.addComponent(ImageGradientComponent);
// 不设置 maskTexture,自动使用代码渐变
gradient.topColor = { r: 1, g: 0.42, b: 0.21, a: 1 };
gradient.bottomColor = { r: 0, g: 0.31, b: 0.54, a: 1 };
gradient.strength = 0.8;

方式四:应用到文本

import { ImageGradientComponent } from "./ImageGradientComponent";

const text = new Laya.Text();
text.text = "渐变文字效果";
text.fontSize = 48;
text.width = 300;
text.height = 100;
text.align = "center";   // 水平居中
text.valign = "middle"; // 垂直居中
Laya.stage.addChild(text);

const gradient = text.addComponent(ImageGradientComponent);
gradient.topColor = { r: 1, g: 0.2, b: 0.5, a: 1 };   // 粉色
gradient.bottomColor = { r: 0.2, g: 0, b: 0.8, a: 1 }; // 紫色
gradient.strength = 1.0;

效果预设

火焰渐变

const gradient = img.addComponent(ImageGradientComponent);
gradient.topColor = { r: 1, g: 0.9, b: 0.2, a: 1 };   // 黄色
gradient.bottomColor = { r: 1, g: 0.2, b: 0, a: 1 };   // 红色
gradient.strength = 0.9;

海洋渐变

const gradient = img.addComponent(ImageGradientComponent);
gradient.topColor = { r: 0.2, g: 0.8, b: 1, a: 1 };    // 浅蓝
gradient.bottomColor = { r: 0, g: 0.1, b: 0.4, a: 1 };  // 深蓝
gradient.strength = 0.7;

森林渐变

const gradient = img.addComponent(ImageGradientComponent);
gradient.topColor = { r: 0.6, g: 0.9, b: 0.4, a: 1 };  // 嫩绿
gradient.bottomColor = { r: 0.1, g: 0.4, b: 0.1, a: 1 }; // 深绿
gradient.strength = 0.8;

夕阳渐变

const gradient = img.addComponent(ImageGradientComponent);
gradient.topColor = { r: 1, g: 0.5, b: 0.2, a: 1 };    // 橙色
gradient.bottomColor = { r: 0.6, g: 0.1, b: 0.4, a: 1 }; // 紫红
gradient.strength = 0.85;

黑白滤镜

const gradient = img.addComponent(ImageGradientComponent);
gradient.topColor = { r: 1, g: 1, b: 1, a: 1 };        // 白色
gradient.bottomColor = { r: 0, g: 0, b: 0, a: 1 };     // 黑色
gradient.strength = 0.5;  // 降低强度获得柔和效果

工作原理

遮罩图模式

当设置了 maskTexture 时,使用灰度遮罩图控制渐变效果:

原图像素     遮罩图亮度    渐变色      最终结果
─────────   ─────────   ─────────   ──────────
  [R,G,B]  ×    亮度     ×  渐变色   =   混合色
                 ↑
        0 = 无渐变效果
        1 = 完全应用渐变

像素混合公式

result = original + (gradient - original) × maskBrightness × strength

遮罩亮度计算(亮度公式):

brightness = (R × 0.299 + G × 0.587 + B × 0.114) / 255

代码渐变模式

当未设置 maskTexture 时,使用代码生成垂直渐变:

渐变计算

对于每个像素 (x, y):
    t = y / height                    // 归一化位置 (0~1)
    gradient = topColor + (bottomColor - topColor) × t
    result = original + (gradient - 128) × strength

图示

顶部颜色 (topColor)
    │
    │ ← 渐变过渡
    │
    │
底部颜色 (bottomColor)

文本处理流程

1. 在 Canvas 上绘制文本
   ↓
2. 获取文本像素数据(getImageData)
   ↓
3. 根据渐变色/遮罩图混合像素
   ↓
4. 将处理后的图像绘制到 Text.graphics

动态更新

修改文本后刷新

const text = new Laya.Text();
const gradient = text.addComponent(ImageGradientComponent);

// 修改文本
text.text = "新的内容";

// 方式一:自动刷新(组件已监听 CHANGE 事件)
// 无需手动调用

// 方式二:手动刷新
gradient.refresh();

动态更改渐变色

const gradient = img.getComponent(ImageGradientComponent) as ImageGradientComponent;

// 更改颜色
gradient.topColor = { r: 1, g: 0, b: 0, a: 1 };
gradient.bottomColor = { r: 0, g: 0, b: 1, a: 1 };

// 重新应用
gradient.refresh();

跨平台兼容性

平台 兼容性 说明
Web 浏览器 ✅ 完全支持 使用 Canvas 2D API
移动端 WebView ✅ 支持 现代浏览器均支持
微信小游戏 ⚠️ 部分支持 toDataURL 可能有安全限制
其他小游戏平台 ⚠️ 需要适配 取决于平台 Canvas 支持

注意事项

  1. 遮罩图要求

    • 推荐使用灰度图(亮度更直观)
    • PNG 格式,支持透明通道
    • 亮度越高,渐变效果越明显
  2. 性能考虑

    • 像素操作在大尺寸图片上可能影响性能
    • 建议图片尺寸不超过 2048×2048
    • 文本内容变化时会自动防抖(100ms 延迟)
  3. 资源加载

    • 图片资源需配置 CORS 以支持跨域
    • 遮罩图需要在组件应用前加载完成
  4. 文本限制

    • Text 组件修改文本后渐变会自动刷新
    • 仅支持单行文本的渐变效果
  5. 颜色格式

    • IDE 中颜色值为 0-1 范围
    • 代码中可直接使用对象格式 {r, g, b, a}
    • 也支持十六进制字符串 "#FF6B35"

常见问题

Q: 为什么图片没有变化?

A: 检查以下几点:

  1. autoApply 是否为 true
  2. strength 值是否为 0
  3. 遮罩图是否正确加载
  4. 原图是否有透明区域

Q: 遮罩图效果不明显?

A: 尝试:

  1. 增加 strength 值(接近 1.0)
  2. 检查遮罩图的亮度分布
  3. 使用对比度更大的渐变色

Q: 文本渐变后不显示?

A: 确保:

  1. Text 组件已设置 widthheight
  2. 文本内容不为空
  3. 颜色值在有效范围内(0-1)

Q: 如何制作遮罩图?

A: 遮罩图制作建议:

┌─────────────┐
│  ████       │  ← 顶部:高亮度(白色)
│  ██████     │
│  ████████   │  ← 中部:中亮度(灰色)
│  ██████████ │
│  ███████████│  ← 底部:低亮度(黑色)
└─────────────┘

Q: 代码渐变和遮罩图模式有什么区别?

A:

特性 代码渐变 遮罩图渐变
渐变形状 线性垂直 由遮罩图决定
灵活度
性能 较快 较慢
适用场景 简单渐变 复杂/不规则渐变

源码详解

组件结构

@regClass()
export class ImageGradientComponent extends Laya.Component {
    // 属性定义
    @property({type: Laya.Texture}) maskTexture: Laya.Texture | null = null;
    @property({type: "Color"}) topColor: any = {r: 255, g: 107, b: 53, a: 1};
    @property({type: "Color"}) bottomColor: any = {r: 0, g: 78, b: 137, a: 1};
    @property({type: Number}) strength: number = 0.8;
    @property({type: Boolean}) tile: boolean = false;
    @property({type: Boolean}) autoApply: boolean = true;
}

装饰器说明

  • @regClass() - LayaAir 3.x 组件注册装饰器,使组件在 IDE 中可见
  • @property() - 属性装饰器,定义可在 IDE 属性面板中编辑的属性

生命周期方法

onAwake()
onAwake() {
    if (this.autoApply) {
        this.apply();  // 自动应用渐变
    }
    // 监听文本变化(Text/Label 专用)
    if (this.owner instanceof Laya.Text || this.owner instanceof Laya.Label) {
        this.owner.on(Laya.Event.CHANGE, this, this.onTextChanged);
    }
}

执行时机:组件加载时自动调用

功能

  1. 根据 autoApply 决定是否自动应用渐变
  2. 为文本组件添加 CHANGE 事件监听

核心方法:apply()

apply() {
    Laya.timer.callLater(this, () => {
        // 1. 判断组件类型
        if (this.owner instanceof Laya.Text || this.owner instanceof Laya.Label) {
            // 文本处理流程
            return;
        }

        // 2. 判断是否有遮罩图
        if (!this.maskTexture) {
            this.applyCodeGradient();  // 代码渐变
            return;
        }

        // 3. 获取纹理
        let texture = ownerAny.texture;
        if (!texture && ownerAny._graphics) {
            texture = ownerAny._graphics.texture;  // 从 AutoBitmap 获取
        }

        // 4. 应用渐变
        this.applyWithTexture(texture);
    });
}

流程图

apply()
    │
    ├─ Text/Label? ──Yes→ applyToText() / applyToTextWithoutMask()
    │                │
    │                └─No
    │
    ├─ 无遮罩图? ────Yes→ applyCodeGradient()
    │                  │
    │                  └─No
    │
    └─ applyWithTexture() → processWithMask() → doProcessWithMask()

像素处理核心算法

代码渐变模式 (doCodeGradient)
private doCodeGradient(sprite: Laya.Sprite, img: HTMLImageElement, width: number, height: number) {
    const canvas = document.createElement('canvas');
    const ctx = canvas.getContext('2d', {willReadFrequently: true})!;

    // 1. 绘制原图到 Canvas
    ctx.drawImage(img, 0, 0, actualWidth, actualHeight);
    const imageData = ctx.getImageData(0, 0, actualWidth, actualHeight);
    const data = imageData.data;

    // 2. 解析渐变色
    const c1 = this.parseColor(this.topColor);    // {r, g, b}
    const c2 = this.parseColor(this.bottomColor); // {r, g, b}

    // 3. 逐像素处理
    for (let y = 0; y < actualHeight; y++) {
        const t = y / actualHeight;  // 归一化位置 0~1
        const gradR = c1.r + (c2.r - c1.r) * t;  // 线性插值
        const gradG = c1.g + (c2.g - c1.g) * t;
        const gradB = c1.b + (c2.b - c1.b) * t;

        for (let x = 0; x < actualWidth; x++) {
            const i = (y * actualWidth + x) * 4;  // 像素索引
            const alpha = data[i + 3];

            if (alpha > 0) {  // 只处理非透明像素
                const r = data[i];
                const g = data[i + 1];
                const b = data[i + 2];

                // 核心混合公式
                data[i]     = r + (gradR - 128) * strength;
                data[i + 1] = g + (gradG - 128) * strength;
                data[i + 2] = b + (gradB - 128) * strength;
            }
        }
    }

    // 4. 写回 Canvas
    ctx.putImageData(imageData, 0, 0);
}

算法图解

渐变色计算(线性插值):
    gradR = c1.r + (c2.r - c1.r) × t
           ↑          ↑
        顶部色     底部色与顶部色的差

    当 t = 0 (顶部): gradR = c1.r
    当 t = 1 (底部): gradR = c2.r
    当 t = 0.5      : gradR = c1.r + (c2.r - c1.r) × 0.5 (中间值)

混合公式:
    result = original + (gradient - 128) × strength

    gradient - 128 的含义:
    - gradient < 128 → 负值 → 使原图变暗
    - gradient > 128 → 正值 → 使原图变亮
    - gradient = 128 → 零   → 保持原图

遮罩图模式 (doProcessWithMask)
private doProcessWithMask(...) {
    // 1. 绘制原图
    ctx.drawImage(sourceImg, 0, 0, actualWidth, actualHeight, 0, 0, actualWidth, actualHeight);
    const originalData = ctx.getImageData(0, 0, actualWidth, actualHeight);

    // 2. 绘制遮罩图(根据 tile 参数决定拉伸或平铺)
    if (this.tile) {
        maskCanvas.width = maskImg.width;
        maskCanvas.height = maskImg.height;
        maskCtx.drawImage(maskImg, 0, 0);  // 平铺:保持原尺寸
    } else {
        maskCanvas.width = actualWidth;
        maskCanvas.height = actualHeight;
        maskCtx.drawImage(maskImg, 0, 0, actualWidth, actualHeight);  // 拉伸
    }
    const maskData = maskCtx.getImageData(...);

    // 3. 逐像素处理
    for (let y = 0; y < actualHeight; y++) {
        const t = y / actualHeight;
        const gradR = c1.r + (c2.r - c1.r) * t;
        // ...

        for (let x = 0; x < actualWidth; x++) {
            const i = (y * actualWidth + x) * 4;

            if (alpha > 0) {
                // 获取遮罩值(平铺模式下使用取模运算)
                const maskX = x % maskCanvas.width;
                const maskY = y % maskCanvas.height;
                const maskI = (maskY * maskCanvas.width + maskX) * 4;

                // 计算遮罩亮度
                const maskBrightness = (
                    maskData.data[maskI] * 0.299 +
                    maskData.data[maskI + 1] * 0.587 +
                    maskData.data[maskI + 2] * 0.114
                ) / 255;

                // 混合原图和渐变色
                result[i] = origR + (gradR - origR) * maskBrightness * strength;
            }
        }
    }
}

遮罩平铺示意

原图:遮罩图
┌─────────┐    ┌───┐
│         │    │███│
│ 512×313 │    │░░░│
│         │    │▓▓▓│
└─────────┘    └───┘
              64×64

平铺模式 (tile=true):
┌─────────┐
│███░░░▓▓▓│  遮罩图重复
│███░░░▓▓▓│
│███░░░▓▓▓│
└─────────┘

拉伸模式 (tile=false):
┌─────────┐
│█████████│  遮罩图拉伸
│░░░░░░░░░│  填充整个
│▓▓▓▓▓▓▓▓▓│  图像
└─────────┘

颜色解析 (parseColor)

private parseColor(color: any): { r: number, g: number, b: number } {
    if (color && typeof color === "object" && color.r !== undefined) {
        // IDE 颜色对象:{r: 1, g: 0.42, b: 0.21, a: 1}
        // 范围是 0-1,需要转换为 0-255
        const r = Math.floor(color.r * 255);
        const g = Math.floor(color.g * 255);
        const b = Math.floor(color.b * 255);
        return {r, g, b};
    }
    if (typeof color === "string") {
        // 十六进制字符串:"#FF6B35"
        const r = parseInt(color.slice(1, 3), 16);
        const g = parseInt(color.slice(3, 5), 16);
        const b = parseInt(color.slice(5, 7), 16);
        return {r, g, b};
    }
    return {r: 0, g: 0, b: 0};
}

颜色格式转换

IDE 输入 (0-1)          Canvas 输出 (0-255)
─────────────────      ─────────────────
{r: 1,   g: 1,   b: 1}  →  {r: 255, g: 255, b: 255} (白色)
{r: 0,   g: 0,   b: 0}  →  {r: 0,   g: 0,   b: 0}   (黑色)
{r: 1,   g: 0.5, b: 0}  →  {r: 255, g: 128, b: 0}   (橙色)

文本处理流程

private applyToTextWithoutMask() {
    // 1. 获取文本属性
    const text = textComp.text;
    const font = textComp.font;
    const fontSize = textComp.fontSize;
    const align = textComp.align;    // left, center, right
    const valign = textComp.valign;  // top, middle, bottom

    // 2. 创建 Canvas 并绘制白色文本
    const canvas = document.createElement('canvas');
    const ctx = canvas.getContext('2d', {willReadFrequently: true})!;
    ctx.font = `${fontSize}px ${font}`;
    ctx.fillStyle = '#FFFFFF';  // 白色文本

    // 3. 计算对齐位置
    let x = 0, y = 0;
    if (align === 'center') x = (compWidth - textWidth) / 2;
    if (align === 'right') x = compWidth - textWidth;
    if (valign === 'middle') y = (compHeight - textHeight) / 2;
    if (valign === 'bottom') y = compHeight - textHeight;

    ctx.fillText(text, x, y);

    // 4. 获取像素并应用渐变
    const imageData = ctx.getImageData(0, 0, compWidth, compHeight);
    // ... 像素处理 ...

    // 5. 将结果绘制到 Text.graphics
    const dataUrl = canvas.toDataURL();
    Laya.loader.load(dataUrl, Laya.Handler.create(this, (tex: Laya.Texture) => {
        textComp.graphics.clear();
        textComp.graphics.drawTexture(tex, 0, 0);
    }));
}

文本对齐计算图示

水平对齐 (align):
┌─────────────────┐
│left    center   │right
│●                │        ●
│         ●       │                ●
└─────────────────┴────────────────

垂直对齐 (valign):
┌───────┐
│   top │  ●
│middle │     ●
│bottom │        ●
└───────┘

关键技术点

1. willReadFrequently 优化
const ctx = canvas.getContext('2d', {willReadFrequently: true})!;

作用:告诉浏览器将频繁调用 getImageData(),浏览器会优化存储格式以提升读取性能。

2. 防抖处理
onTextChanged() {
    if (this._refreshTimer) {
        Laya.timer.clear(this, this._refreshTimer);
    }
    this._refreshTimer = Laya.timer.once(100, this, this.apply);
}

作用:文本快速变化时,避免频繁执行像素操作,100ms 内只执行最后一次。

3. 纹理获取兼容性
let texture = ownerAny.texture;
if (!texture && ownerAny._graphics) {
    texture = ownerAny._graphics.texture;  // Image 组件的 AutoBitmap
}
if (!texture && ownerAny.skin) {
    texture = Laya.Loader.getRes(ownerAny.skin);  // 从缓存获取
}

支持场景

  • Sprite 直接赋值 texture
  • Image 组件使用 skin 属性
  • Label 组件(使用 Text 子节点)
4. 原图尺寸处理
// 使用原图的实际尺寸,而不是 texture 报告的尺寸
const actualWidth = sourceImg.width;
const actualHeight = sourceImg.height;

原因:LayaAir 的 texture 可能来自图集(Atlas),texture.width 是裁剪后的尺寸,而 sourceImg 是完整原图。


API 参考

属性

属性 类型 说明
maskTexture Texture | null 遮罩纹理
topColor any 顶部颜色 {r,g,b,a}
bottomColor any 底部颜色 {r,g,b,a}
strength number 渐变强度 0-1
tile boolean 遮罩图平铺模式
autoApply boolean 自动应用

方法

方法 参数 说明
refresh() - 手动刷新渐变效果
apply() - 应用渐变到目标对象
Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐