1. 效果展示
2. 技术细节
2.1. Viewport
A viewport represents a polygonal (normally rectangular) area in computer graphics that is currently being viewed. In web browser terms, it refers to the part of the document you're viewing which is currently visible in its window (or the screen, if the document is being viewed in full screen mode). Content outside the viewport is not visible onscreen until scrolled into view.
- Visual Viewport
- The portion of the viewport that is currently visible is called the visual viewport. This can be smaller than the layout viewport, such as when the user has pinched-zoomed. The visual viewport is the visual portion of a screen excluding on-screen keyboards, areas outside of a pinch-zoom area, or any other on-screen artifact that doesn't scale with the dimensions of a page.
- Layout viewport
- The layout viewport is the viewport into which the browser draws a web page. Essentially, it represents what is available to be seen, while the visual viewport represents what is currently visible on the user's display device.
This becomes important, for example, on mobile devices, where a pinching gesture can usually be used to zoom in and out on a site's contents. The rendered document doesn't change in any way, so the layout viewport remains the same as the user adjusts the zoom level. Instead, the visual viewport is updated to indicate the area of the page that they can see.
2.2. Element.getBoundingClientRect()
The Element.getBoundingClientRect() method returns the size of an element and its position relative to the viewport.
- The element's size is equal to its width/height padding border-width in the case that the standard box model is being used, or width/height only if box-sizing: border-box has been set on it.
- The result is the smallest rectangle which contains the entire element, with read-only left, top, right, bottom, x, y, width, and height properties describing the overall border-box in pixels. Properties other than width and height are relative to the top-left of the viewport.
示例:
代码语言:javascript复制<!DOCTYPE html>
<html>
<head>
<title></title>
<style type="text/css">
html,
body {
width: 100%;
height: 100%;
}
body {
margin: 0;
}
.box {
position: absolute;
left: 300px;
top: 300px;
width: 400px;
height: 200px;
padding: 20px;
margin: 100px auto;
background: purple;
}
</style>
</head>
<body>
<div class="box"></div>
<div class="result"></div>
<script type="text/javascript">
let elem = document.querySelector('div.box');
let rect = elem.getBoundingClientRect();
const resultDiv = document.querySelector("div.result");
resultDiv.innerHTML = "";
for (var key in rect) {
if(typeof rect[key] !== 'function') {
let para = document.createElement('div');
para.textContent = `${ key } : ${ rect[key] }`;
resultDiv.appendChild(para);
}
}
</script>
</body>
</html>
2.3. Touch
- Touch Interface
- The Touch interface represents a single contact point on a touch-sensitive device. The contact point is commonly a finger or stylus and the device may be a touchscreen or trackpad.
- TouchEvent
- The TouchEvent interface represents an UIEvent which is sent when the state of contacts with a touch-sensitive surface changes. This surface can be a touch screen or trackpad, for example. The event can describe one or more points of contact with the screen and includes support for detecting movement, addition and removal of contact points, and so forth.
- Touches are represented by the Touch object; each touch is described by a position, size and shape, amount of pressure, and target element. Lists of touches are represented by TouchList objects.
- touchmove event
- Sent when the user moves a touch point along the surface. The event's target is the same element that received the touchstart event corresponding to the touch point, even if the touch point has moved outside that element.
示例:
代码语言:javascript复制<!DOCTYPE html>
<html>
<head>
<title></title>
<style type="text/css">
#wrapper{
width: 600px;
height: 600px;
margin: 50px auto;
border: 3px solid blue;
}
#viewer{
position: fixed;
top: 50%;
transform: translate(0, -50%);
border: 1px solid green;
}
</style>
</head>
<body>
<div id="wrapper">
</div>
<pre id="viewer">
</pre>
<script type="text/javascript">
const ele = document.getElementById("wrapper");
const viewer = document.getElementById("viewer");
ele.addEventListener("touchmove", function(e) {
e.preventDefault();
var touch = e.targetTouches[0];
if (touch) {
viewer.innerHTML = `touchmove:
clientX:${touch.clientX}
clientY:${touch.clientY}`;
}
}, false);
</script>
</body>
</html>
2.4. Canvas
2.4.1. 填充图片
CanvasRenderingContext2D.drawImage():
- The CanvasRenderingContext2D.drawImage() method of the Canvas 2D API provides different ways to draw an image onto the canvas
语法:
代码语言:javascript复制void ctx.drawImage(image, dx, dy);
void ctx.drawImage(image, dx, dy, dWidth, dHeight);
void ctx.drawImage(image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight);
示例:
代码语言:javascript复制<!DOCTYPE html>
<html>
<head>
<title></title>
</head>
<body>
<canvas id="canvas"></canvas>
<script type="text/javascript">
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
const image = new Image(60, 45); // Using optional size for image
image.onload = function(){ // Draw when image has loaded
// Use the intrinsic size of image in CSS pixels for the canvas element
canvas.width = this.naturalWidth;
canvas.height = this.naturalHeight;
// Will draw the image as 300x227, ignoring the custom size of 60x45
// given in the constructor
ctx.drawImage(this, 0, 0);
// To use the custom size we'll have to specify the scale parameters
// using the element's width and height properties - lets draw one
// on top in the corner:
ctx.drawImage(this, 0, 0, this.width, this.height);
};
// Load an image of intrinsic size 300x227 in CSS pixels
image.src = 'https://media.prod.mdn.mozit.cloud/attachments/2013/06/22/5397/7a3ec0cae64a95ad454ac3bc2c71c004/rhino.jpg';
</script>
</body>
</html>
2.4.2. 填充文本
CanvasRenderingContext2D.fillText():
- The CanvasRenderingContext2D method fillText(), part of the Canvas 2D API, draws a text string at the specified coordinates, filling the string's characters with the current fillStyle. An optional parameter allows specifying a maximum width for the rendered text, which the user agent will achieve by condensing the text or by using a lower font size.
语法:
代码语言:javascript复制CanvasRenderingContext2D.fillText(text, x, y [, maxWidth]);
示例:
代码语言:javascript复制<!DOCTYPE html>
<html>
<head>
<title></title>
</head>
<body>
<canvas id="canvas" width="400" height="150"></canvas>
<script type="text/javascript">
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
ctx.font = '50px serif';
ctx.fillText('Hello world', 50, 60);
ctx.fillText('Hello world', 50, 120, 140);
</script>
</body>
</html>
2.4.3. 绘制圆弧路径
CanvasRenderingContext2D.arc():
- The CanvasRenderingContext2D.arc() method of the Canvas 2D API adds a circular arc to the current sub-path.
语法:
代码语言:javascript复制void ctx.arc(x, y, radius, startAngle, endAngle [, anticlockwise]);
- The rect() method creates a rectangular path whose starting point is at (x, y) and whose size is specified by width and height.
示例:
代码语言:javascript复制<!DOCTYPE html>
<html>
<head>
<title></title>
</head>
<body>
<canvas id="canvas" width="400" height="150"></canvas>
<script type="text/javascript">
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
ctx.rect(10, 20, 150, 100);
ctx.stroke();
</script>
</body>
</html>
2.4.4. 合成操作
CanvasRenderingContext2D.globalCompositeOperation:
- The CanvasRenderingContext2D.globalCompositeOperation property of the Canvas 2D API sets the type of compositing operation to apply when drawing new shapes.
语法:
3. 综合示例
代码语言:javascript复制<!DOCTYPE html>
<html>
<head>
<title></title>
<meta name="viewport" content="width=device-width,initial-scale=1.0,maximum-scale=1.0,user-scalable=0" />
<style>
.bg {
position: fixed;
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, .2);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.scratch-card{
width: 2.96rem;
height: 3.45rem;
background: url(./red-envelope.png) center center/contain no-repeat;
position: relative;
}
.scratch-card > .scrape-area {
width: 2.40rem;
height: 1.20rem;
position: absolute;
bottom: 1rem;
left: 50%;
transform: translate(-50%, 0);
}
.scratch-card > .scrape-area > .award {}
.scratch-card > .scrape-area > canvas {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
}
.scratch-card > .scrape-area > canvas.finish {
display: none;
}
.scratch-card > .footer {
position: absolute;
left: 0;
bottom: 0;
width: 100%;
}
</style>
</head>
<body>
<div class="bg">
<div class="scratch-card">
<div class="scrape-area">
<div class="award">
<h1>奖品:旋风冲锋</h1>
</div>
<canvas id="scrape-canvas"></canvas>
</div>
<img class="footer" src="footer.png"/>
</div>
</div>
<script type="text/javascript">
!function(e, t) {
var n = t.documentElement,d = e.devicePixelRatio || 1;
function i() {
var e = n.clientWidth / 3.75;
n.style.fontSize = e "px"
}
t.body.style.fontSize = "16px";
i();
e.addEventListener("resize", i);
e.addEventListener("pageshow", function(e) {
e.persisted && i()
});
2 <= d;
} (window, document)
</script>
<script>
function initFestivalCanvas(){
var remain = 100; // (次)
const bridge = document.getElementById("scrape-canvas");
const bridgeCanvas = bridge.getContext('2d');
const img = new Image();
img.onload = function(){
bridgeCanvas.drawImage(img, 0, 0, bridge.width, bridge.height);
bridgeCanvas.font = "36px bold 微软雅黑";
bridgeCanvas.fillStyle = "red";
bridgeCanvas.fillText("刮我一下", 75, 90);
}
img.src = './scrape-area-inner.png';
function detectLeftButton(event) {
if ('buttons' in event) {
return event.buttons === 1;
} else if ('which' in event) {
return event.which === 1;
} else {
return event.button === 1;
}
}
function getBrushPos(xRef, yRef) {
var bridgeRect = bridge.getBoundingClientRect();
return {
x: xRef-bridgeRect.left,
y: yRef-bridgeRect.top
};
}
function drawDot(mouseX,mouseY){
bridgeCanvas.beginPath();
bridgeCanvas.arc(mouseX, mouseY, 15, 0, 2*Math.PI, true);
bridgeCanvas.fillStyle = '#000';
bridgeCanvas.globalCompositeOperation = "destination-out";
bridgeCanvas.fill();
}
bridge.addEventListener("touchmove", function(e) {
e.preventDefault();
const touch = e.targetTouches[0];
if (touch) {
const brushPos = getBrushPos(touch.clientX, touch.clientY);
drawDot(brushPos.x, brushPos.y);
if(remain-- < 0){
const ele = document.getElementById("scrape-canvas");
ele.className = ele.className " " "finish";
}
}
}, false);
}
initFestivalCanvas();
</script>
</body>
</html>
参考:
Element.getBoundingClientRect(): https://developer.mozilla.org/en-US/docs/Web/API/Element/getBoundingClientRect Viewport: https://developer.mozilla.org/en-US/docs/Glossary/Viewport https://developer.mozilla.org/en-US/docs/Glossary/visual_viewport https://developer.mozilla.org/en-US/docs/Glossary/Layout_viewport A tale of two viewports: https://www.quirksmode.org/mobile/viewports.html https://www.quirksmode.org/mobile/viewports2.html Touch: https://developer.mozilla.org/en-US/docs/Web/API/Touch https://developer.mozilla.org/zh-CN/docs/Web/API/TouchEvent CanvasRenderingContext2D.arc() https://developer.mozilla.org/zh-CN/docs/Web/API/CanvasRenderingContext2D/arc globalCompositeOperation: https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/globalCompositeOperation