我有一个刮刮卡模拟器。
用户应该能够单击并拖动以显示下面的文本。
我对这个实现有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);
}());
答案 0 :(得分:4)
这里有几个问题:
clearTo()
也采用填充样式颜色,但尝试将其加载为图像getBoundingClientRect()
is relative to client。fillStyle
并使用fillRect()
清除。如果不使用drawImage()
。window.onmousemove
代替(不是必需的,但在这种情况下是一个更好的选项,因为它会将光标完全移到画布外面 - 可选地使用更宽的父节点 - 这取决于你......)。 / LI>
clientX
和clientY
代替并始终进行计算。ImageData
并计算没有alpha数据的像素(= 0)。然后将此计数除以总像素数以获得覆盖百分比。这很快(我将在下面展示如何)。所以,让我们稍微修改一下结构。这不是最佳选择,但意味着让您入门。将图像加载一次并全局加载(或在父作用域内加载,以便可以访问该对象)。
// 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>