HTML5 Canvas: 当图像改变时画在图像上的文本停止显示

HTML5 Canvas: Text drawn on image stops showing when image is changed

所以我有这个简单的图像编辑器,我在其中使用 canvas 来绘制用户选择的图像和一些文本。也就是说,用户可以上传图片,然后如果他们愿意,可以添加文本或只更改图片的渐变。

目前,除了有一个问题外,该应用程序运行良好。

如何发现问题?执行以下操作:

  1. 上传一张随机图片。
  2. 在输入字段中输入内容。
  3. 上传并更改图像。

您会看到文字消失了。

这是代码:

const canvasTxt                 = window.canvasTxt.default;
const canvas                    = document.getElementById('canvas');
const ctx                       = canvas?.getContext('2d');
const btnDownload               = document.querySelector('.btnDownload');
const fileUpload                = document.querySelector('.file-upload');

const text1                     = document.getElementById('text1');
const textForm1                 = document.getElementById('text1-form');
const text2                     = document.getElementById('text2');
const textForm2                 = document.getElementById('text2-form');
const text2ShadowColor          = document.getElementById('text2shadowcolor');
const text2ShadowOffsetY        = document.getElementById('text2shadowoffy');

const imageForm                 = document.getElementById('image-form');
const imageGrad                 = document.getElementById('gradientcolor');
const imageGradOpacity          = document.getElementById('gradientopacity');


$(fileUpload).on('change', function(e) {
      let imgObj          = new Image();
      imgObj.onload       = draw;
      imgObj.onerror      = failed;
      imgObj.src          = URL.createObjectURL(this.files[0]);

      imgManipulation( e, imgObj );
});    

const imgManipulation = ( e, imgObj ) => {
    $(textForm1).on('change keyup input', updateCanvas);
    $(textForm2).on('change keyup input', updateCanvas);
    $(imageForm).on('change keyup input', updateCanvas);

    function updateCanvas(e) {
        e.preventDefault();
        ctx.clearRect(0, 0, canvas.width, canvas.height);
        ctx.drawImage(imgObj, 0, 0);

        createGradient($(imageGrad).val(), $(imageGradOpacity).val());

  
        // TEXT1 STYLES based on user input
        canvasTxt.fontSize      = 70;
        ctx.fillStyle           = "white";
        canvasTxt.drawText(
            ctx, 
            $(text1).val(), 
            0, 
            0, 
            200, 
            200
        );


        // TEXT2 STYLES
        canvasTxt.font          = 50;
        ctx.fillStyle           = "white";
        canvasTxt.drawText(
            ctx, 
            $(text2).val(),
            20, 
            20, 
            200, 
            200
        );
    }
};

function hexToRgb(hex) {
    var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
    return result ? {
        r: parseInt(result[1], 16),
        g: parseInt(result[2], 16),
        b: parseInt(result[3], 16)
    } : null;
};

function createGradient(hex, alpha) {

    const r = hexToRgb(hex).r.toString();
    const g = hexToRgb(hex).g.toString();
    const b = hexToRgb(hex).b.toString();

    var gradient =  ctx.createLinearGradient(800, 0, 0, 0);
    gradient.addColorStop(0, `rgba(${r}, ${g}, ${b}, ${alpha})`);

    ctx.save() // <----------- ADD
    ctx.fillStyle = gradient;
    ctx.fillRect(0, 0, canvas.width, canvas.height);
    ctx.restore() // <----------- ADD
};


function draw() {
    canvas.width        = this.naturalWidth;
    canvas.height       = this.naturalHeight;
    const nw            = this.naturalWidth;
    const nh            = this.naturalHeight;

    ctx.drawImage(this, 0, 0, nw, nh);
};

function failed() {
    console.error("The provided file couldn't be loaded as an Image media");
};


$(btnDownload).on('click', function(e) {
    const a = document.createElement('a');
    document.body.appendChild(a);
    a.href = canvas.toDataURL();
    a.download = "canvas-image.png";
    a.click();
    document.body.removeChild(a);
});    
#canvas{
    background-color: transparent; 
    width: 30%; 
    height: auto;
    border: 1px solid #777;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
<script src="https://unpkg.com/canvas-txt@3.0.0/build/index.js"></script>

<canvas id="canvas" width="800" height="500"></canvas>

<div>
    <input type="file" class="file-upload" />
    <button class="btnDownload">Download</button>
</div>


<div>
  <form id="text1-form">
    <input type="text" id="text1" placeholder="text 1"/> 
  </form>
</div>

<div>
  <form id="text2-form">
    <input type="text" id="text2" placeholder="text 2"/> 
  </form>
</div>

<div>
  <h2>Image Gradient and Opacity</h2>
  <form id="image-form">
    <input type="color" id="gradientcolor" value="#000000" />
    <input type="range" id="gradientopacity" min="0" max="1" value="0" step="0.05" />
  </form>
</div>


<div>
  <h2>Text2 Shadow Offset X</h2>
  <input type="color" id="text2shadowcolor" value="#000000" />
  <input type="range" id="text2shadowoffy" min="0" max="40" value="0" />
</div>

代码概要:

1:首先我有 fileUpload 事件侦听器。它从用户那里获取图像并创建一个图像对象并将其绘制在 canvas 上。然后以 imgObj 和事件作为参数调用 imgManipulation 函数。

imgManipulation 函数从文本的输入事件侦听器开始。也就是说,只要输入发生变化,即用户写了一些东西,就会调用 updateCanvas 函数。

updateCanvas 函数实际上是在图像上绘制文本。我正在使用一个名为 canvasTxt 的包,它可以帮助文本变成多行。

更改图像后,如果用户在之前输入过用户文本的输入字段上写了一个字母,文本就会神奇地出现。

如何在不删除用户输入的文本的情况下更改图像?

非常感谢您的帮助。

提前致谢

存在计时问题。

在此代码中:

$(fileUpload).on('change', function(e) {
      let imgObj          = new Image();
      imgObj.onload       = draw;
      imgObj.onerror      = failed;
      imgObj.src          = URL.createObjectURL(this.files[0]);

      imgManipulation( e, imgObj );
});

您要求在加载图像时执行绘图功能。然后您调用 imgManipulation,它会正确地绘制文本,但它会在图像有机会加载之前立即调用。所以它写入文本(仍然保存在输入元素中)但是当加载图像并调用绘图时,它会被覆盖。

由于 draw 只是将图像写入大多数图像的 canvas,这将覆盖包含文本的 canvas 的顶部。

因此,在加载新图像时,您需要更新整个 canvas,包括文本。

注意:每次用户选择新图像时都会创建一个新的 img 元素。只有一个图像元素并为其提供新来源可能是明智的,否则长时间的用户会话可能会不必要地填满存储空间。

此代码段通过在加载新图像时设置超时并在发生时调用更新 canvas 来说明计时问题。显然,对于生产版本,重构代码以便更新发生在正在加载(或已经加载)的图像上是明智的。

text2 的 fontSize 设置还有一个问题,它试图将字体设置为 50 - 这会影响后续重绘时使用的字体大小,因此已更改为 fontSize。 Text2 也覆盖了 text1 的一部分,但这是一个不同的问题。

        const canvasTxt                 = window.canvasTxt.default;
    const canvas                    = document.getElementById('canvas');
    const ctx                       = canvas?.getContext('2d');
    const btnDownload               = document.querySelector('.btnDownload');
    const fileUpload                = document.querySelector('.file-upload');

    const text1                     = document.getElementById('text1');
    const textForm1                 = document.getElementById('text1-form');
    const text2                     = document.getElementById('text2');
    const textForm2                 = document.getElementById('text2-form');
    const text2ShadowColor          = document.getElementById('text2shadowcolor');
    const text2ShadowOffsetY        = document.getElementById('text2shadowoffy');

    const imageForm                 = document.getElementById('image-form');
    const imageGrad                 = document.getElementById('gradientcolor');
    const imageGradOpacity          = document.getElementById('gradientopacity');

          let imgObj          = new Image();

    $(fileUpload).on('change', function(e) {
          imgObj.onload       = draw;
          imgObj.onerror      = failed;
          imgObj.src          = URL.createObjectURL(this.files[0]);

          setTimeout(function() {imgManipulation(e, imgObj);}, 1000);// timeout set just to illustrate the timing problem (1sec = long enough for img to load normally)
          //imgManipulation( e, imgObj );
    });    

    const imgManipulation = ( e, imgObj ) => {
        $(textForm1).on('change keyup input', updateCanvas);
        $(textForm2).on('change keyup input', updateCanvas);
        $(imageForm).on('change keyup input', updateCanvas);
        updateCanvas(e);// force an update of canvas each time called, not just on keyups
        function updateCanvas(e) {
            e.preventDefault();
            ctx.clearRect(0, 0, canvas.width, canvas.height);
            ctx.drawImage(imgObj, 0, 0);

            createGradient($(imageGrad).val(), $(imageGradOpacity).val());

      
            // TEXT1 STYLES based on user input
            canvasTxt.fontSize      = 70;
            ctx.fillStyle           = "white";
            canvasTxt.drawText(
                ctx, 
                $(text1).val(), 
                0, 
                0, 
                200, 
                200
            );


            // TEXT2 STYLES
            canvasTxt.fontSize          = 50;//was just 'font'
            ctx.fillStyle           = "white";
            canvasTxt.drawText(
                ctx, 
                $(text2).val(),
                20, 
                20, 
                200, 
                200
            );
        }
    };

    function hexToRgb(hex) {
        var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
        return result ? {
            r: parseInt(result[1], 16),
            g: parseInt(result[2], 16),
            b: parseInt(result[3], 16)
        } : null;
    };

    function createGradient(hex, alpha) {

        const r = hexToRgb(hex).r.toString();
        const g = hexToRgb(hex).g.toString();
        const b = hexToRgb(hex).b.toString();

        var gradient =  ctx.createLinearGradient(800, 0, 0, 0);
        gradient.addColorStop(0, `rgba(${r}, ${g}, ${b}, ${alpha})`);

        ctx.save() // <----------- ADD
        ctx.fillStyle = gradient;
        ctx.fillRect(0, 0, canvas.width, canvas.height);
        ctx.restore() // <----------- ADD
    };


    function draw() {
        canvas.width        = this.naturalWidth;
        canvas.height       = this.naturalHeight;
        const nw            = this.naturalWidth;
        const nh            = this.naturalHeight;

        //ctx.drawImage(this, 0, 0, nw, nh);
    };

    function failed() {
        console.error("The provided file couldn't be loaded as an Image media");
    };


    $(btnDownload).on('click', function(e) {
        const a = document.createElement('a');
        document.body.appendChild(a);
        a.href = canvas.toDataURL();
        a.download = "canvas-image.png";
        a.click();
        document.body.removeChild(a);
    });
#canvas{
        background-color: transparent; 
        width: 30%; 
        height: auto;
        border: 1px solid #777;
    }
</style>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
    <script src="https://unpkg.com/canvas-txt@3.0.0/build/index.js"></script>
    
  <canvas id="canvas" width="800" height="500"></canvas>

    <div>
        <input type="file" class="file-upload" />
        <button class="btnDownload">Download</button>
    </div>


    <div>
      <form id="text1-form">
        <input type="text" id="text1" placeholder="text 1"/> 
      </form>
    </div>

    <div>
      <form id="text2-form">
        <input type="text" id="text2" placeholder="text 2"/> 
      </form>
    </div>

    <div>
      <h2>Image Gradient and Opacity</h2>
      <form id="image-form">
        <input type="color" id="gradientcolor" value="#000000" />
        <input type="range" id="gradientopacity" min="0" max="1" value="0" step="0.05" />
      </form>
    </div>


    <div>
      <h2>Text2 Shadow Offset X</h2>
      <input type="color" id="text2shadowcolor" value="#000000" />
      <input type="range" id="text2shadowoffy" min="0" max="40" value="0" />
    </div>

你可以看到有两种绘制图像的方法:imgObj.onload = draw;updateCanvas

因此,当更改图像时,draw 调用的函数没有填充文本。

最好把canvas的绘图东西放在同一个函数里

像这样。

const canvasTxt                 = window.canvasTxt.default;
const canvas                    = document.getElementById('canvas');
const ctx                       = canvas?.getContext('2d');
const btnDownload               = document.querySelector('.btnDownload');
const fileUpload                = document.querySelector('.file-upload');

const text1                     = document.getElementById('text1');
const textForm1                 = document.getElementById('text1-form');
const text2                     = document.getElementById('text2');
const textForm2                 = document.getElementById('text2-form');
const text2ShadowColor          = document.getElementById('text2shadowcolor');
const text2ShadowOffsetY        = document.getElementById('text2shadowoffy');

const imageForm                 = document.getElementById('image-form');
const imageGrad                 = document.getElementById('gradientcolor');
const imageGradOpacity          = document.getElementById('gradientopacity');


$(fileUpload).on('change', function(e) {
      let imgObj          = new Image();
      imgObj.onload       = draw;
      imgObj.onerror      = failed;
      imgObj.src          = URL.createObjectURL(this.files[0]);

      imgManipulation( e, imgObj );
});  

// -----AND `reDraw` function
function reDraw (imgObj) {
        ctx.clearRect(0, 0, canvas.width, canvas.height);
        ctx.drawImage(imgObj, 0, 0);

        createGradient($(imageGrad).val(), $(imageGradOpacity).val());

  
        // TEXT1 STYLES based on user input
        canvasTxt.fontSize      = 70;
        ctx.fillStyle           = "white";
        canvasTxt.drawText(
            ctx, 
            $(text1).val(), 
            0, 
            0, 
            200, 
            200
        );


        // TEXT2 STYLES 
        canvasTxt.fontSize      = 50; // canvasTxt.font = 50;
        ctx.fillStyle           = "white";
        canvasTxt.drawText(
            ctx, 
            $(text2).val(),
            20, 
            20, 
            200, 
            200
        );
}

const imgManipulation = ( e, imgObj ) => {
    $(textForm1).on('change keyup input', updateCanvas);
    $(textForm2).on('change keyup input', updateCanvas);
    $(imageForm).on('change keyup input', updateCanvas);

    function updateCanvas(e) {
        e.preventDefault();
        reDraw(imgObj) // <-------- CALL `reDraw`
    }
};

function hexToRgb(hex) {
    var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
    return result ? {
        r: parseInt(result[1], 16),
        g: parseInt(result[2], 16),
        b: parseInt(result[3], 16)
    } : null;
};

function createGradient(hex, alpha) {

    const r = hexToRgb(hex).r.toString();
    const g = hexToRgb(hex).g.toString();
    const b = hexToRgb(hex).b.toString();

    var gradient =  ctx.createLinearGradient(800, 0, 0, 0);
    gradient.addColorStop(0, `rgba(${r}, ${g}, ${b}, ${alpha})`);

    ctx.save()
    ctx.fillStyle = gradient;
    ctx.fillRect(0, 0, canvas.width, canvas.height);
    ctx.restore()
};


function draw() {
    canvas.width        = this.naturalWidth;
    canvas.height       = this.naturalHeight;
    // const nw            = this.naturalWidth;
    // const nh            = this.naturalHeight;

    // ctx.drawImage(this, 0, 0, nw, nh);
    reDraw(this) // <-------- CALL `reDraw`
};

function failed() {
    console.error("The provided file couldn't be loaded as an Image media");
};


$(btnDownload).on('click', function(e) {
    const a = document.createElement('a');
    document.body.appendChild(a);
    a.href = canvas.toDataURL();
    a.download = "canvas-image.png";
    a.click();
    document.body.removeChild(a);
}); 
#canvas{
  background-color: transparent; 
  width: 30%; 
  height: auto;
  border: 1px solid #777;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
<script src="https://unpkg.com/canvas-txt@3.0.0/build/index.js"></script>

<canvas id="canvas" width="800" height="500"></canvas>

<div>
    <input type="file" class="file-upload" />
    <button class="btnDownload">Download</button>
</div>


<div>
  <form id="text1-form">
    <input type="text" id="text1" placeholder="text 1"/> 
  </form>
</div>

<div>
  <form id="text2-form">
    <input type="text" id="text2" placeholder="text 2"/> 
  </form>
</div>

<div>
  <h2>Image Gradient and Opacity</h2>
  <form id="image-form">
    <input type="color" id="gradientcolor" value="#000000" />
    <input type="range" id="gradientopacity" min="0" max="1" value="0" step="0.05" />
  </form>
</div>


<div>
  <h2>Text2 Shadow Offset X</h2>
  <input type="color" id="text2shadowcolor" value="#000000" />
  <input type="range" id="text2shadowoffy" min="0" max="40" value="0" />
</div>