`
madfroghe
  • 浏览: 122149 次
  • 性别: Icon_minigender_1
  • 来自: 杭州
社区版块
存档分类
最新评论

Flash/Flex学习笔记(37):坐标旋转

阅读更多

坐标旋转是个啥概念呢?

如上图,(蓝色)小球 绕某一中心点旋转a角度后,到达(红色)小球的位置,则红色小球相对中心点的坐标为:

x1 = dx * cos(a) - dy * sin(a)

y1 = dy * cos(a) + dx * sin(a)

这个就是坐标旋转公式,如果要反向旋转,则公式要修正一下,有二种方法:

1.将a变成-a,即:

x1 = dx * cos(-a) - dy * sin(-a)

y1 = dy * cos(-a) + dx * sin(-a)

2.将正向旋转公式中的相减号交换

x1 = dx * cos(a) + dy * sin(a);
y1 = dy * cos(a) - dx * sin(a);

先来回顾一个经典的小球圆周运动:


var ball:Ball = new Ball(10);


var centerX:Number = stage.stageWidth/2;

var centerY:Number = stage.stageHeight/2;

var radius:Number = 50;


var angle:Number = 0;


addChild(ball);


addEventListener(Event.ENTER_FRAME,EnterFrameHandler);

 
ball.x = centerX + Math.cos(angle) * radius;

 
ball.y = centerY + Math.sin(angle) * radius;


graphics.lineStyle(1,0x999999);

 
graphics.moveTo(ball.x,ball.y);

 
function EnterFrameHandler(e:Event):void{   

ball.x = centerX + Math.cos(angle) * radius;

 
ball.y = centerY + Math.sin(angle) * radius;

 
angle += 0.02;

 
if (angle<=2*Math.PI+0.02){

 
graphics.lineTo(ball.x,ball.y);

 
}


}

这个没啥特别的,接下来我们用坐标旋转公式换一种做法验证一下是否有效:

 
var ball:Ball = new Ball(10);

 

 
var centerX:Number = stage.stageWidth/2;

 
var centerY:Number = stage.stageHeight/2;


var radius:Number = 50;

 
var angle:Number = 0;

 
ball.vr = 0.02;//旋转角速度

 
ball.x = centerX + radius;


ball.y = centerY;

 
var cos:Number = Math.cos(ball.vr);

 
var sin:Number = Math.sin(ball.vr);

addChild(ball);

 
addEventListener(Event.ENTER_FRAME,EnterFrameHandler);


graphics.lineStyle(1,0x999999);

 
graphics.moveTo(ball.x,ball.y);

 
var i:Number = 0;

 
function EnterFrameHandler(e:Event):void{   

 
var dx:Number = ball.x - centerX; 

 
var dy:Number = ball.y - centerY; 

 
var x2:Number = cos * dx - sin * dy; 

 
var y2:Number = cos * dy + sin * dx; 

 
ball.x = centerX + x2; 

 
ball.y = centerY + y2;

 
i++;

 
if (i<=(2*Math.PI+ball.vr)/ball.vr){


trace(i);


graphics.lineTo(ball.x,ball.y);

 
}

 
}

效果完全相同,说明坐标旋转公式完全是有效的,问题来了:原本一个简单的问题,经过这样复杂的处理后,效果并没有变化,为何要化简为繁呢?

好处1:提高运行效率

下面演示的多个物体旋转的传统做法:

 
var arrBalls:Array = new Array(30);

 
var centerX:Number = stage.stageWidth/2;

 
var centerY:Number = stage.stageHeight/2;

 
for(var i:uint=0,j:uint=arrBalls.length;i<j;i++){

 
arrBalls[i] = new Ball(3 + Math.random()*5,Math.random()*0xffffff);

 
arrBalls[i].x = centerX + 100 * (Math.random()*2-1);


arrBalls[i].y = centerY + 100 * (Math.random()*2-1);

 
addChild(arrBalls[i]);


}

 
graphics.lineStyle(1);

 
graphics.moveTo(centerX,centerY-5);

 
graphics.lineTo(centerX,centerY+5);


graphics.moveTo(centerX-5,centerY);

 
graphics.lineTo(centerX+5,centerY);

 
addEventListener(Event.ENTER_FRAME,EnterFrameHandler);

 
function EnterFrameHandler(e:Event):void{

for(var i:uint=0,j:uint=arrBalls.length;i<j;i++){

 
var ball:Ball = arrBalls[i];

 
var dx:Number = ball.x - stage.stageWidth/2;

 
var dy:Number = ball.y - stage.stageHeight/2;

 
var dist:Number = Math.sqrt(dx*dx + dy*dy); //1次Math调用


ball.vr = Math.atan2(dy,dx);//2次Math调用


ball.vr += 0.005;

 
ball.x = centerX + dist * Math.cos(ball.vr);//3次Math调用

 
ball.y = centerY + dist * Math.sin(ball.vr);//4次Math调用      

 
}   

 
}

坐标旋转的新做法:

 
var arrBalls:Array = new Array(30);


var centerX:Number = stage.stageWidth/2;

 
var centerY:Number = stage.stageHeight/2;


var vr:Number = 0.01;


for(var i:uint=0,j:uint=arrBalls.length;i<j;i++){

 
arrBalls[i] = new Ball(3 + Math.random()*5,Math.random()*0xffffff);

 
arrBalls[i].x = centerX + 100 * (Math.random()*2-1);


arrBalls[i].y = centerY + 100 * (Math.random()*2-1);


arrBalls[i].vr = vr;

 
addChild(arrBalls[i]);


}

 
graphics.lineStyle(1);

 
graphics.moveTo(centerX,centerY-5);

 
graphics.lineTo(centerX,centerY+5);

 
graphics.moveTo(centerX-5,centerY);

 
graphics.lineTo(centerX+5,centerY);

 
addEventListener(Event.ENTER_FRAME,EnterFrameHandler);

 
//将Math函数的调用放到到循环体外

 
var cos:Number = Math.cos(vr);


var sin:Number = Math.sin(vr);

 
function EnterFrameHandler(e:Event):void{


for(var i:uint=0,j:uint=arrBalls.length;i<j;i++){

 
var ball:Ball = arrBalls[i];

 
var dx:Number = ball.x - stage.stageWidth/2;

 
var dy:Number = ball.y - stage.stageHeight/2;

 
var x2:Number = cos * dx - sin * dy; 

 
var y2:Number = cos * dy + sin * dx;

 
ball.x = centerX + x2;

 
ball.y = centerY + y2;      

 
}   


}

对比代码可以发现,同样的效果用坐标旋转处理后,Math的调用全部提升到循环外部了,对于30个小球来讲,每一帧至少减少了30 * 4 = 120次的三角函数运算

好处2:可以方便的处理斜面反弹

先来看下正向/反向旋转的测试


var ball:Ball=new Ball(15);

 
addChild(ball);

 
var centerX:Number=stage.stageWidth/2;

 
var centerY:Number=stage.stageHeight/2;


var radius:Number=100;


ball.x=centerX+radius;


ball.y=centerY;

 
graphics.lineStyle(1,0xdddddd);

 
graphics.moveTo(centerX,centerY);

 
graphics.lineTo(ball.x,ball.y);

 
graphics.lineStyle(1);


graphics.moveTo(centerX,centerY -10);

 
graphics.lineTo(centerX,centerY +10);

 
graphics.moveTo(centerX-10,centerY);

 
graphics.lineTo(centerX+10,centerY);

 
var angle:Number=30*Math.PI/180;

 
btn1.addEventListener(MouseEvent.MOUSE_DOWN,btn1Click);

 
//旋转

 
function btn1Click(e:MouseEvent):void {


var cos:Number=Math.cos(angle);

 
var sin:Number=Math.sin(angle);


var dx:Number=ball.x-centerX;

 
var dy:Number=ball.y-centerY;

 
var x1:Number=dx*cos-dy*sin;

 
var y1:Number=dy*cos+dx*sin;


ball.x=centerX+x1;

 
ball.y=centerY+y1;

 
graphics.lineStyle(1,0xdddddd);

 
graphics.moveTo(centerX,centerY);

 
graphics.lineTo(ball.x,ball.y);

 
}

 
btn2.addEventListener(MouseEvent.MOUSE_DOWN,btn2Click);

 
//反转1

 
function btn2Click(e:MouseEvent):void {

 
var dx:Number=ball.x-centerX;

 
var dy:Number=ball.y-centerY;

 
var cos:Number=Math.cos(-angle);

 
var sin:Number=Math.sin(-angle);

 
var x1:Number=dx*cos-dy*sin;

 
var y1:Number=dy*cos+dx*sin;


ball.x=centerX+x1;

 
ball.y=centerY+y1;

 
graphics.lineStyle(1,0xdddddd);

 
graphics.moveTo(centerX,centerY);

graphics.lineTo(ball.x,ball.y);


}

 
btn3.addEventListener(MouseEvent.MOUSE_DOWN,btn3Click);

 
//反转2

 
function btn3Click(e:MouseEvent):void{

 
var dx:Number=ball.x-centerX;

 
var dy:Number=ball.y-centerY;

 
var cos:Number=Math.cos(angle);

 
var sin:Number=Math.sin(angle);

 
//反转公式

 
var x1:Number=dx*cos+dy*sin;

 
var y1:Number=dy*cos-dx*sin;

 
ball.x=centerX+x1;


ball.y=centerY+y1;

 
graphics.lineStyle(1,0xdddddd);

graphics.moveTo(centerX,centerY);


graphics.lineTo(ball.x,ball.y);

 
}

对于水平或垂直的反弹运动,实现起来并不复杂,但对于斜面而言,情况就复杂多了,首先:物体反弹并不是光学中的反射,所以用“入射角=反射角”来模拟并不准确,其次我们还要考虑到重力因素/摩擦力因素,这些都会影响到速度的大小和方向。

如果用坐标旋转的思维方式去考虑这一复杂的问题,解决办法就变得非常简单。

所有向量(物理学中也常称矢量,虽然这二者在严格意义上讲并不相同)都可应用坐标旋转,我们可以把整个系统(包括斜面以及相对斜面运行物体的速度向量)都通过坐标旋转变成水平面或垂直面,这样就把问题简单化了,等一切按水平或垂直的简单方式处理完成以后,再把系统旋转回最初的样子。


package {

 
import flash.display.Sprite;

 
import flash.events.Event;


import flash.events.MouseEvent;

 
import flash.ui.Mouse;

 
import flash.ui.MouseCursor;

 
import flash.geom.Rectangle;


public class AngleBounce extends Sprite {

 
private var ball:Ball;

 
private var line:Sprite;

 
private var gravity:Number=0.25;

 
private var bounce:Number=-0.6;

 
private var rect:Rectangle;

 
public function AngleBounce() {


init();

 
}

 
private function init():void {

Mouse.cursor=MouseCursor.BUTTON;


ball=new Ball(10);


addChild(ball);

 
ball.x=100;


ball.y=100;

 
line=new Sprite  ;

 
line.graphics.lineStyle(1);

 
line.graphics.lineTo(300,0);

 
addChild(line);

 
line.x=50;

line.y=200;


line.rotation=25;//将line旋转形成斜面


stage.addEventListener(MouseEvent.MOUSE_DOWN,MouseDownHandler);

 
rect = line.getBounds(this);//获取line的矩形边界

 
graphics.beginFill(0xefefef)

 
graphics.drawRect(rect.left,rect.top,rect.width,rect.height);

 
graphics.endFill();

}

 
private function MouseDownHandler(e:Event) {


addEventListener(Event.ENTER_FRAME,EnterFrameHandler);


}

 
private function EnterFrameHandler(e:Event):void {

//line.rotation = (stage.stageWidth/2 - mouseX)*0.1;


//普通的运动代码 

ball.vy+=gravity;

ball.x+=ball.vx;

 
ball.y+=ball.vy;

 
/*//只有二者(的矩形边界)碰撞了才需要做处理

 
if (ball.hitTestObject(line)) {*/


//也可以换成下面的方法检测          


if (ball.x > rect.left && ball.x < rect.right && ball.y >rect.top && ball.y < rect.bottom){


//trace("true");

 
//获得角度及正余弦值 


var angle:Number=line.rotation*Math.PI/180;

 
var cos:Number=Math.cos(angle);

 
var sin:Number=Math.sin(angle);

 
//获得 ball 与 line 的相对位置 

 
var dx:Number=ball.x-line.x;

 
var dy:Number=ball.y-line.y;


//反向旋转坐标(得到ball“相对”斜面line的坐标)

var x2:Number=cos*dx+sin*dy;

 
var y2:Number=cos*dy-sin*dx;

 
//反向旋转速度向量(得到ball“相对”斜面的速度) 


var vx2:Number=cos*ball.vx+sin*ball.vy;

 
var vy2:Number=cos*ball.vy-sin*ball.vx;

 
//实现反弹 


if (y2>- ball.height/2) {

 
y2=- ball.height/2;


vy2*=bounce;

 
//将一切再正向旋转回去

 
dx=cos*x2-sin*y2;

 
dy=cos*y2+sin*x2;

 
ball.vx=cos*vx2-sin*vy2;


ball.vy=cos*vy2+sin*vx2;

 
//重新定位


ball.x=line.x+dx;

 
ball.y=line.y+dy;

 
}

 
}

//跑出舞台边界后将其重新放到原始位置

 
if (ball.x>=stage.stageWidth-ball.width/2||ball.y>=stage.stageHeight-ball.height/2) {

 
ball.x=100;

ball.y=100;

 
ball.vx=0;

 
ball.vy=0;

 
removeEventListener(Event.ENTER_FRAME,EnterFrameHandler);

 
}


}

 
}

}

多角度斜面反弹:


package {

 
import flash.display.Sprite;

 
import flash.events.Event;

 
import flash.display.StageScaleMode;

 
import flash.display.StageAlign;


import flash.geom.Rectangle;

 
import flash.events.MouseEvent;

 
import flash.ui.Mouse;

 
import flash.ui.MouseCursor;


public class MultiAngleBounce extends Sprite {

 
private var ball:Ball;


private var lines:Array;

 
private var numLines:uint=5;

 
private var gravity:Number=0.3;

 
private var bounce:Number=-0.6;

 
public function MultiAngleBounce() {

 
init();

 
}

 
private function init():void {

 
stage.scaleMode=StageScaleMode.NO_SCALE;

 
stage.align=StageAlign.TOP_LEFT;

 
ball=new Ball(20);

 
addChild(ball);

 
ball.x=100;

 
ball.y=50;

 
// 创建 5 个 line 影片 


lines = new Array();


for (var i:uint = 0; i < numLines; i++) {

 
var line:Sprite = new Sprite();

 
line.graphics.lineStyle(1);

 
line.graphics.moveTo(-50, 0);


line.graphics.lineTo(50, 0);

 
addChild(line);

 
lines.push(line);

 
}

 
// 放置并旋转 

 
lines[0].x=100;

 
lines[0].y=100;

 
lines[0].rotation=30;


lines[1].x=100;

 
lines[1].y=230;

 
lines[1].rotation=45;

 
lines[2].x=250;

 
lines[2].y=180;

 
lines[2].rotation=-30;

 
lines[3].x=150;

 
lines[3].y=330;

 
lines[3].rotation=10;

 
lines[4].x=230;

 
lines[4].y=250;


lines[4].rotation=-30;

 
addEventListener(Event.ENTER_FRAME, onEnterFrame);


ball.addEventListener(MouseEvent.MOUSE_DOWN,MouseDownHandler);

 
ball.addEventListener(MouseEvent.MOUSE_OVER,MouseOverHandler);

 
stage.addEventListener(MouseEvent.MOUSE_UP,MouseUpHandler);

 
}


function MouseOverHandler(e:MouseEvent):void {

 
Mouse.cursor=MouseCursor.HAND;

 
}

 
function MouseDownHandler(e:MouseEvent):void {

 
Mouse.cursor=MouseCursor.HAND;

 
var bounds:Rectangle = new Rectangle(ball.width,ball.height,stage.stageWidth-2*ball.width,stage.stageHeight-2*ball.height);

 
ball.startDrag(true,bounds);

 
removeEventListener(Event.ENTER_FRAME, onEnterFrame);


}

 
function MouseUpHandler(e:MouseEvent):void {

 
ball.stopDrag();

 
ball.vx=0;

 
ball.vy=0;

 
Mouse.cursor=MouseCursor.AUTO;

 
addEventListener(Event.ENTER_FRAME, onEnterFrame);

 
}

 
private function onEnterFrame(event:Event):void {

 
// normal motion code 

 
ball.vy+=gravity;


ball.x+=ball.vx;

 
ball.y+=ball.vy;

 
// 舞台四周的反弹 

 
if (ball.x+ball.radius>stage.stageWidth) {

 
ball.x=stage.stageWidth-ball.radius;

 
ball.vx*=bounce;

 
} else if (ball.x - ball.radius < 0) {

 
ball.x=ball.radius;

 
ball.vx*=bounce;

 
}

 
if (ball.y+ball.radius>stage.stageHeight) {

 
ball.y=stage.stageHeight-ball.radius;

 
ball.vy*=bounce;

 
} else if (ball.y - ball.radius < 0) {


ball.y=ball.radius;

 
ball.vy*=bounce;


}

 
// 检查每条线 
for (var i:uint = 0; i < numLines; i++) {

 
checkLine(lines[i]);

 
}

 
}

 
private function checkLine(line:Sprite):void {


// 获得 line 的边界 

 
var bounds:Rectangle=line.getBounds(this);

 
if (ball.x>bounds.left&&ball.x<bounds.right) {

 
// 获取角度与正余弦值 

 
var angle:Number=line.rotation*Math.PI/180;

var cos:Number=Math.cos(angle);

 
var sin:Number=Math.sin(angle);


// 获取 ball 与 line 的相对位置 

 
var x1:Number=ball.x-line.x;

 
var y1:Number=ball.y-line.y;

 
// 旋转坐标 

 
var y2:Number=cos*y1-sin*x1;

 
// 旋转速度向量 

 
var vy1:Number=cos*ball.vy-sin*ball.vx;

 
// 实现反弹 

 
if (y2>- ball.height/2&&y2<vy1) {

 
// 旋转坐标 

 
var x2:Number=cos*x1+sin*y1;

 
// 旋转速度向量 

 
var vx1:Number=cos*ball.vx+sin*ball.vy;


y2=- ball.height/2;


vy1*=bounce;

 
// 将一切旋转回去 

 
x1=cos*x2-sin*y2;

 
y1=cos*y2+sin*x2;

 
ball.vx=cos*vx1-sin*vy1;


ball.vy=cos*vy1+sin*vx1;

 
ball.x=line.x+x1;


ball.y=line.y+y1;

 
}

 
}

 
}

 
}

 
}

分享到:
评论

相关推荐

    Flash/Flex 框架应用 Cairngorm、Mate、PureMVC以及Swiz 的典型例子

    附件是关于 Flash/Flex 几个重要框架 Cairngorm、Mate、PureMVC以及Swiz 的典型例子,由 Tony Hillerson 提供 Homepage: http://insideria.com

    flex/Flash开发系列书籍:WEB3D应用研究

    flex/Flash开发系列书籍:基于FLASH的WEB3D应用研究

    flash/flex画曲线,绘图板

    在IT行业中,Flash/Flex是一种基于ActionScript编程语言和Adobe Flex框架的开发工具,用于创建交互式的、富媒体的Web应用程序。"Flash/Flex画曲线,绘图板"这个主题涉及的是如何使用这些技术来创建一个允许用户自由...

    RE/flex lexical analyzer generator:以正则表达式为中心的快速词法分析器生成器,用于C ++-开源

    语言:C ++许可证:BSD-3代码质量:A + https://lgtm.com/projects/g/Genivia/RE-flex/context:cpp文档:https://www.genivia.com/doc/reflex/html /index.html存储库:https://github.com/Genivia/RE-flex更改日志...

    [Flash/Flex] 使用css定义文本样式

    xmlns:s="library://ns.adobe.com/flex/spark" applicationStyleSheet="@Embed('path/to/your/styles.css')"&gt; ... &lt;/s:Application&gt; ``` 然后在`styles.css`中定义样式,如: ```css .myTextStyle { color: #FF...

    FDT-flash/flex devtoolkit for eclipse.

    **FDT - 一款强大的Flash/Flex开发工具集** FDT(Flash Development Tool)是一款专为Adobe Flash和Flex开发者设计的集成开发环境(IDE),它基于Eclipse平台,提供了高效、专业的开发工具和服务。FDT的出现极大地...

    flash/flex 的aqua皮肤

    在IT行业中,Flash/Flex是一种广泛使用的开发工具,主要用于创建交互式、富媒体的Web应用程序。Flex是基于ActionScript和MXML的开放源代码框架,它允许开发者构建可自定义的用户界面,而Flash则是其背后的动画和...

    FLEX安装方法 集成到eclipse中

    FLEX 安装方法 集成到 eclipse 中 ...* 汉化 FLEX 尤其是 FLEX/AIR 方面的中文资料 * 原创的关于 FLEX 的博客:http://liguoliang.com/ * Adobe 公司 FLEX 主页:http://www.adobe.com/cn/products/flex/

    Flex:登录

    标题“Flex:登录”指的是使用Adobe Flex技术实现用户登录功能的一种方法。Flex是Adobe公司推出的一款基于ActionScript的开源框架,主要用于...压缩包中的"Flex Login"可能包含示例代码或项目结构,供读者学习和参考。

    flex学习笔记 flex学习总结 flex学习教程

    Flex Builder(现已被Adobe Flash Builder取代)是一个集成开发环境,提供了代码提示、调试和项目管理等功能,使得开发更加高效。 3. **Flex组件库**:Flex提供了丰富的预定义组件,如Button、Label、Canvas等,可...

    RTMP直播例子--基于FLASH/FLEX(含源代码) 下载

    在这个“RTMP直播例子--基于FLASH/FLEX(含源代码) 下载”中,我们可以深入探讨RTMP直播的基本原理、FLEX编程以及如何在实际项目中应用这些技术。 首先,RTMP协议的工作原理是通过建立一个持久性的TCP连接,允许...

    解决flash/flex/as3 访问中文域名时的流错误示例

    在《潮汕IT男》网站的文章《解决flash/flex/as3 访问中文域名时的流错误》中,作者陈林生提供了详细的步骤和代码示例,帮助开发者理解和解决这个问题。文章地址是:[http://chenlinsheng.com/?p=990]...

    FLEX学习笔记

    《FLEX学习笔记》 FLEX,全称为Flex Builder,是由Adobe公司开发的一款基于MXML和ActionScript的开源框架,用于构建富互联网应用程序(RIA)。它允许开发者创建具有交互性、响应性和丰富用户体验的Web应用。FLEX的...

    Flex学习笔记.rar

    本压缩包“Flex学习笔记.rar”显然是一份针对初学者的教程资料,旨在帮助新接触Flex的开发者快速上手。 在“FlexBeginner.pdf”这份文档中,你可以期待找到以下关键知识点: 1. **Flex概述**:介绍Flex技术的基本...

    flex开源项目介绍.doc

    Flex开源项目介绍 Flex是一种用于构建富互联网应用程序(RIA)的开源框架,它基于ActionScript 3(AS3)编程语言和MXML标记语言。这些开源项目为开发者提供了丰富的组件库、工具和框架,帮助他们扩展Flex的功能,...

    4个简单的Flex例子(包含custom-class-mapping)共享

    总共有4个例子: 1.http://127.0.0.1:8080/flexDemo/HelloWorld/HelloWorld.html ...如果你的数据库配置和我的不一样,请修改flexDemo\WEB-INF\classes\下的DBSetting.properties文件,数据库建表的sql语句是user.sql

    flex4cookbook

    xmlns:s="library://ns.adobe.com/flex/spark" xmlns:mx="library://ns.adobe.com/flex/mx" creationComplete="app_creationCompleteHandler(event)"&gt; &lt;fx:Script&gt; &lt;![CDATA[ import mx.events.FlexEvent; ...

    Flex画 坐标轴曲线 项目用过的 flex4 开发

    在Flex4开发中,创建坐标轴曲线图是一种常见的需求,特别是在数据可视化和图表展示的应用中。Flex是一个基于ActionScript和MXML的开放源代码框架,主要用于构建富互联网应用程序(RIA)。它提供了强大的图形和组件库...

    flex开发系列书籍:WebGIS开发实战

    本篇文章将对 Flex 开发系列书籍:WebGIS 开发实战进行详细的知识点总结,涵盖 Flex 概述、RIA 概述、Flex 开发基础、Flex 开发实践、Flex 与 WebGIS 开发框架、基于 Flex 的 WebGIS 基础开发、基于 Flex 的 WebGIS ...

    Flex与Java的交互

    Flex与Java的交互是跨平台应用开发中的常见技术组合,允许前端用户界面(UI)与后端业务逻辑进行高效沟通。在本文中,我们将深入探讨如何使用Flex 4与Java进行通信,并通过三种不同的方法实现这一目标:RemoteObject...

Global site tag (gtag.js) - Google Analytics