使用Canvas的代码中的一些错误

时间:2017-07-23 19:15:00

标签: javascript canvas

我有一个刮刮卡模拟器。

用户应该能够单击并拖动以显示下面的文本。

我对这个实现有2个错误:

a)有时一旦光标从左或右进入画布,刮刮卡就会自动清除。它应该只在大部分卡被刮掉时自行清除。目前,它仅在用户从顶部边框向下移动光标时才有效。

b)有时刮刮卡根本不起作用,或刮擦将偏离光标,但仅当浏览器窗口小于文档大小时(例如,浏览器窗口宽300px但身体有一个最小宽度为900px或其他用户必须将卡片滚动到视图中。

(function () {
    "use strict";
    var container = document.getElementById('cbox-canvas'),
        arrow = document.getElementById('cbox-arrow'),
        textOne = document.getElementById('cbox-text-1'),
        textTwo = document.getElementById('cbox-text-2'),
        boxOne = document.getElementById('cbox-box-1'),
        boxTwo = document.getElementById('cbox-box-2'),
        cnv = container.getElementsByTagName('canvas'),
        imageCover;

    function createCanvas(parent, width, height) {
        var canvas = {};
        canvas.node = document.createElement('canvas');
        canvas.context = canvas.node.getContext('2d');
        canvas.node.width = width || 100;
        canvas.node.height = height || 100;
        parent.appendChild(canvas.node);
        return canvas;
    }

    function init(container, width, height, fillColor) {
        var canvas = createCanvas(container, width, height),
            ctx = canvas.context;

        // define a custom fillCircle method
        ctx.fillCircle = function (x, y, radius, fillColor) {
            //this.fillStyle = fillColor;
            this.shadowBlur = 15;
            this.shadowOffsetX = 0;
            this.shadowOffsetY = 0;
            this.shadowColor = fillColor;
            this.beginPath();
            this.moveTo(x, y);

            this.arc(x, y, radius, 0, Math.PI * 2, false);
            this.fill();
            this.stroke();
        };
        ctx.clearTo = function (fillColor) {

            var imageObj = new Image();

            imageObj.onload = function () {
                ctx.drawImage(imageObj, 0, 0);
            };
            imageObj.src = fillColor;
        };
        ctx.clearTo(fillColor || "#ddd");

        // bind mouse events
        canvas.node.onmousemove = function (e) {
            var canvasRect = container.getBoundingClientRect(),
                x = e.pageX - canvasRect.left,
                y = e.pageY - canvasRect.top,
                radius = 30,
                calc = 0;

            fillColor = '#ff0000';

            ctx.globalCompositeOperation = 'destination-out';
            ctx.fillCircle(x, y, radius, fillColor);

            calc += x;
            if (calc > 330 || calc < 6) {
                container.removeChild(cnv[0]);
                arrow.className += " slide-it";
                textOne.className += " reveal-it";
                textTwo.className += " fade-in";
                boxOne.className += " fade-in-two";
                boxTwo.className += " fade-in-one";
            }

        };

        container.onmousemove = function (e) {
            var canvasRect = container.getBoundingClientRect(),
                mouseX = e.pageX || e.clientX,
                mouseY = e.pageY || e.clientY,
                relMouseX = mouseX - canvasRect.left,
                relMouseY = mouseY - canvasRect.top,
                leftLimit = 37,
                topLimit = 37,
                rightLimit = 25,
                bottomLimit = 44,
                x = e.pageX - canvasRect.left,
                y = e.pageY - canvasRect.top,
                radius = 25;

            fillColor = '#ff0000';

            if (relMouseX < leftLimit) {
                relMouseX = leftLimit;
            }
            if (relMouseY < topLimit) {
                relMouseY = topLimit;
            }
            if (relMouseX > width - rightLimit) {
                relMouseX = width - rightLimit;
            }
            if (relMouseY > height - bottomLimit) {
                relMouseY = height - bottomLimit;
            }

            if (!canvas.isDrawing) {
                return;
            }

            ctx.globalCompositeOperation = 'destination-out';
            ctx.fillCircle(x, y, radius, fillColor);

        };
    }

    imageCover = "images/scratch.png";
    init(container, 369, 371, imageCover);
}());

https://jsfiddle.net/p05kg0vq/

2 个答案:

答案 0 :(得分:4)

问题

这里有几个问题:

  1. 您正在加载每次异步发生的清晰图像,并且可能无法及时显示。
  2. 同样的方法clearTo()也采用填充样式颜色,但尝试将其加载为图像
  3. 您正在收听鼠标移动事件两个不需要的地方
  4. 改进:你正在画布上听鼠标移动事件。这没有错,但使用窗口对象会更流畅
  5. 您正在使用pageX/Y来计算鼠标位置。这些是相对于页面而非客户端。 getBoundingClientRect() is relative to client
  6. 不确定您打算如何计算承保范围
  7. 还有额外的重构空间。
  8. 解决方案

    1. 全局加载图片并使用该对象作为参数而不是URL。
    2. 区分图像和颜色字符串。为此,请检查参数是否为字符串,如果是,请设置fillStyle并使用fillRect()清除。如果不使用drawImage()
    3. 从容器中删除事件。它不是必需的,会与第二个听众发生冲突。
    4. 使用window.onmousemove代替(不是必需的,但在这种情况下是一个更好的选项,因为它会将光标完全移到画布外面 - 可选地使用更宽的父节点 - 这取决于你......)。 / LI>
    5. 使用clientXclientY代替并始终进行计算。
    6. 提取ImageData并计算没有alpha数据的像素(= 0)。然后将此计数除以总像素数以获得覆盖百分比。这很快(我将在下面展示如何)。
    7. 留给OP改进:)
    8. 所以,让我们稍微修改一下结构。这不是最佳选择,但意味着让您入门。将图像加载一次并全局加载(或在父作用域内加载,以便可以访问该对象)。

      // preload image once
      var imageCover = "//i.imgur.com/b4m1M1n.png"; // needed cors for demo
      var imageObj = new Image();
      imageObj.onload = go;
      imageObj.crossOrigin = "";                    // for demo, for getImageData to work
      imageObj.src = imageCover;
      
      function go() {
      
        /* ... inner code not shown ... */
      
        init(container, 369, 371, imageObj);
      };
      

      然后重写clearTo()以接受图像和填充样式。请注意,这可能会破坏浏览器优化,因为涉及两种不同的类型,但在这种情况下,它可能无关紧要:

      ctx.clearTo = function(fillColor) {
          if (typeof fillColor === "string") {  // is a string?
            ctx.fillStyle = fillColor;          // set as fill style
            ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height);
          }
          else {                                // assumes an image if not string
            ctx.drawImage(fillColor, 0, 0);
          }
      };
      ctx.clearTo(fillColor || "#ddd");
      

      然后将onmousemove移至window个对象并使用clientX / clientY

      window.onmousemove = function(e) {
        var canvasRect = container.getBoundingClientRect(),
          x = e.clientX - canvasRect.left,   // use clientX/Y (pageXY is unofficial)
          y = e.clientY - canvasRect.top,
          /* ... */
      

      在同一代码块中提供一个函数来计算canvas的实时覆盖率:

       // calc converage and clean if < 20%
              if (calcCover(ctx) < 0.2) {
          // end, reveal, etc.
          ctx.clearRect(0,0,ctx.canvas.width,ctx.canvas.height);
          console.log("DONE");
        };
      

      这里使用的功能是:

      function calcCover(ctx) {
        var w = ctx.canvas.width,  // just to cache width/height
            h = ctx.canvas.height,
            // convert Uint8ClampedArray to Uint32Array, no memory loss
            // but faster
            data32 = new Uint32Array(ctx.getImageData(0,0,w,h).data.buffer),
            count = w * h;   // total number of pixels
      
        // iterate, check for alpha-channel (0xAABBGGRR, little-endian format)
        // for the Uint32Array data.
        for(var i = 0; i < data32.length; i++) if (!(data32[i] & 0xff000000)) count--;
      
        // convert to a percentage (or rather normalize)
        return count / (w*h);
      };
      

      现在我们很高兴:

      <强> Modified fiddle

答案 1 :(得分:1)

查看您的代码我只能建议完全重写

;(function () {
    "use strict";
    // Generic functions and constants
    const PI2 = Math.PI * 2;
    function applyStyle (ctx, style) { Object.keys(style).forEach(key => ctx[key] = style[key] ) }
    function ease (val, power) { return val < 0 ? 0 : val > 1 ? 1 : Math.pow(val, power) }
    
    // General settings 
    const settings = {
        width : 369,
        height : 371,
        coveragedMin : 0.2, // when to uncover all out of 1
        coverColor : "#ddd",  // colour to show on canvas while main image is loading. (not needed but to keep with you code)
        mouseEvents : "mouseup,mousedown,mousemove".split(","),  // list of mouse events to listen to
        coverImage : loadImage("https://image.ibb.co/f8TNS5/scratch.png"),  // the scratch image
        container : document.getElementById('cbox-canvas'),  // the container
        drawStyle : {  // the draw style of the revealing mouse moves. Note that this adds radius to the context but should not matter
            radius : 20,
            shadowBlur : 15,
            shadowOffsetX : 0,
            shadowOffsetY : 0,
            shadowColor : "black",
            fillStyle : "black",
            globalCompositeOperation : "destination-out",
        },
        startAnim (){    // specific to this scratch reveal animations
            document.getElementById("cbox-arrow").className = "cbox-arrow  slide-it";
            document.getElementById("cbox-text-1").className = "cbox-text-1 reveal-it";
            document.getElementById("cbox-box-1").className = "cbox-box-1 fade-in-two"; 
            document.getElementById("cbox-box-2").className = "cbox-box-2 fade-in-one";
            document.getElementById("cbox-text-2").className = "cbox-text-2 fade-in";
        },
        coverageArray : (() => {const buf = new Uint8Array(64); buf.fill(1); return buf }) (), // array to is used to determine coverage        
    }
    var update = true; // when true update canvas render
    const mouse = { x : 0, y : 0, button : false}; // Mouse state
    function mouseEvent (e) { // handles all mouse events
        const canvasRect = settings.container.getBoundingClientRect();
        mouse.x = e.pageX - canvasRect.left - scrollX;
        mouse.y = e.pageY - canvasRect.top - scrollY;
        if (e.type === "mousedown") { mouse.button = true }
        else if (e.type === "mouseup") { mouse.button = false }
        update = true;  // flags that there needs to be a re render
    }
    function fillCircle (ctx, x, y, style) { // Draws a circle on context ctx, at location x,y using style 
        applyStyle(ctx, style);
        ctx.beginPath();
        ctx.arc(x, y, style.radius, 0, Math.PI * 2);
        ctx.fill();
    }
    function setCoverage (array,x,y){  // Clears the coverage array, coordinates x,y are normalised 0-1
        var i = array.length - 1;     // and returns coverage as a value 0 no coverage to 1 full cover
        const size = Math.sqrt(array.length) | 0;
        array[(x * size) | 0 + ((y * size) | 0) * size] = 0;
        var count = 0;
        while(i-- > 0){ count += array[i] };
        return count / array.length;
    }
    function loadImage (url) { // Loads an image and sets a property indicating if its has been rendered
        const image = new Image();
        image.src = url;
        image.rendered = false;
        return image;
    }
    function createCanvas (width, height) { // Creates a canvas of size width and height, set property ctx to the 2D context
        const canvas = document.createElement('canvas');
        canvas.width = width;
        canvas.height = height;
        canvas.ctx = canvas.getContext("2d");
        return canvas;
    }
    (function (settings) { //  Start the app
        const canvas = createCanvas(settings.width, settings.height);
        settings.container.appendChild(canvas);
        const ctx = canvas.ctx;
        settings.mouseEvents.forEach(eventName => addEventListener(eventName, mouseEvent)); // start the mouse
        var reveal = false;  // when true reveal the prize (yep suckers) ???
        var fade = 1;   // fades out the canvas 
        ctx.fillStyle = settings.coverColor;  // cover while waiting for image to load
        ctx.fillRect(0, 0, canvas.width, canvas.height); // image will not yet have loaded so cover image
        (function mainLoop () {  // main animation loop will play unt ill canvas faded out
            if (settings.coverImage.complete && !settings.coverImage.rendered) { // wait till image has loaded
                ctx.globalCompositeOperation = "source-over";
                ctx.drawImage(settings.coverImage, 0, 0, settings.width, settings.height);
                settings.coverImage.rendered = true;
                const swipeEl = document.getElementById("swipe-area");
                swipeEl.className = "swipe-area loaded";
                swipeEl.title = "Use your mouse to reveal your PRIZE :P";
            }
            if (update) {  // only if needed render canvas
                if (settings.coverImage.rendered) {
                    mouse.button && fillCircle(ctx, mouse.x, mouse.y, settings.drawStyle);
                    setCoverage(settings.coverageArray, mouse.x /  settings.width, mouse.y / settings.height) < settings.coveragedMin && (reveal = true);
                    update = false;
                }
                if (reveal) {
                    fade -= 0.05;
                    canvas.style.opacity = ease(fade,2);
                    update = true; // need continuous update for animation
                }
            }
            if (reveal && fade <= 0) { // scratching all done remove canvas, mouse events and start any animations. Do not call requestAnimationFrame as all done. 
                const swipeEl = document.getElementById("swipe-area");
                swipeEl.style.cursor = "pointer";
                swipeEl.title = "Click here to collect your $$$$";
                settings.container.removeChild(canvas);
                settings.mouseEvents.forEach(eventName => removeEventListener(eventName, mouseEvent));
                settings.startAnim();
                // All done. All objects should now have no references (important to remove mouse and requestAnimation frame) and any other functions
                // that can hold a closure 
            } else {              
                requestAnimationFrame(mainLoop);
            }
        } () );
    } (settings) );
} () );
/*SWIPE*/

.swipe-area {
  position: absolute;
  width: 369px;
  height: 371px;
  left: 10px;
  top: 10px;
  z-index: 15;
  background-size: 100%;
}
.preload {
  cursor : wait;
  background: url('https://image.ibb.co/f8TNS5/scratch.png') no-repeat;
  background-size: 100%;
 }
.loaded {
  cursor : url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAbCAYAAABm409WAAABkklEQVRIibXWPWtUQRjF8d/GbDRFomghZFNorREMiIUgpEgRSFJFuyAWNoovTQIqdvZp8lXCBb+ApZ9AApb2lmFS3Jns3XVm9ya7+8DDwMA9/3vOM/eFfC0W9qdSodHTFz46FkIQfv4S8GdaoCQY6AMaoIkgoapqodQJkgCTQgbE/4ecTOwkC+hDTi76qpAiYBAysHc6M0gIM3MxOPjLQkYCcpBLA4aPagHyNwGqasYurgRo6SIH6UzVRVX1OwKWcAMLmBsFOGvrYgiwhvtYxU3MTxRVxsVvbGEDD3Ab10qAzriomoCj4wsX+9jDM/RiXNmax+NxTsRvRwNwgI94GSNbUhj+Qszx7SgnCdCYwzd8xms8wS2FgSfAOr4348jF1BD/isMY1XrUyAK60d4aXuCH/NFMfYgvcf3UJqK56KKnHtge3uDfkPAHvI/rO7yKNzR2yKkWsYKHeI5NbGMHu3HdiXubeIpHUbz1b0+K62688F6he7iD5TZ3Plwd9dHt4nqhu+oHq/U7KQfpqOeT66LwOUobOkEQCJp1AAAAAElFTkSuQmCC') 9 20, pointer;

  background: url('https://image.ibb.co/j4j7uk/sc_bg.png') no-repeat;
  background-size: 100%;  
}
.anim-container {
  position: absolute;
  width: 360px;
  height: 366px;
  right: 5px;
  top: 5px;
  z-index: -1;
  background-size: 100%;
  overflow: hidden;
}

.cbox-arrow {
  position: absolute;
  left: 56px;
  top: 0;
  z-index: -1;
  width: 260px;
  height: 264px;
  background: url('https://image.ibb.co/fXKwn5/arrow.png') no-repeat;
  background-size: 100%;
  -webkit-animation: 10s slide;
}

.cbox-text-1 {
  position: absolute;
  left: 72px;
  top: 100px;
  z-index: -1;
  width: 230px;
  height: 65px;
  background: url('https://image.ibb.co/d6YYZk/test1.png') no-repeat;
  background-size: 100%;
  opacity: 1;
}

.cbox-text-2 {
  position: absolute;
  left: 72px;
  top: 100px;
  z-index: -1;
  width: 230px;
  height: 65px;
  background: url('https://image.ibb.co/bCaQfQ/test2.png') no-repeat;
  background-size: 100%;
  opacity: 0;
}

.cbox-box-1 {
  position: absolute;
  left: 55px;
  top: 167px;
  z-index: -1;
  width: 257px;
  height: 65px;
  background: url('https://image.ibb.co/fG7hS5/box1.png') no-repeat;
  background-size: 100%;
  opacity: 0;
}

.cbox-box-2 {
  position: absolute;
  left: 135px;
  top: 124px;
  z-index: -1;
  width: 99px;
  height: 127px;
  background: url('https://image.ibb.co/dOSSuk/box2.png') no-repeat;
  background-size: 100%;
  opacity: 0;
}
.hidden {  display: none;}
/* unknowns */

.newslisting, #sidebar1Bottom { background: #ffffff !important; }
.tmx header { height: 269px;}

/*Animations, you can add agent prescripts, though we should never have to do that */
.fade-in { animation: 1.5s 2.5s fade;}
.fade-in-one { animation: 2.5s 5s fade;}
.fade-in-two { animation: 2.5s 7.5s fade-alt forwards;}
.fade-in-three { animation: 5s 15s fade;}
.reveal-it { animation: 2.5s reveal forwards;}
.slide-it { animation: 5s slide-in forwards;}
@keyframes fade-alt {0% { opacity: 0; } 10% {opacity : 1;} 100% {  opacity: 1;} }
@keyframes fade { 0% { opacity: 0; } 10% {opacity: 1;} 90% {opacity: 1;} 100% {opacity: 0;} }
@keyframes reveal {0% {  opacity: 1;} 80% { opacity: 1;} 100% { opacity: 0;} }
@keyframes slide-in {0% { top: 5px; }  80% { top: 5px; } 100% { top: -150px;} }
<div class="swipe-area preload" id="swipe-area" title="Just a moment as we asses your gullibility!">
  <div id="cbox-canvas">
    <div class="anim-container">
      <div id="cbox-arrow" class="hidden"></div>
      <div id="cbox-text-1" class="hidden"></div>
      <div id="cbox-box-1" class="hidden"></div>
      <div id="cbox-box-2" class="hidden"></div>
      <div id="cbox-text-2" class="hidden"></div>
    </div>
  </div>
</div>