整合营销服务商

电脑端+手机端+微信端=数据同步管理

免费咨询热线:

如何用不到 2KB 的 JavaScript 代码写一个 3D 赛车游戏?

个月前,JS1k游戏制作节(JS1K game jam)传出不再举办消息后,许多游戏迷开始哀嚎。

Frank Force 也是其中一位,但他还有另一层身份——一位德克萨斯州奥斯汀的独立游戏设计师。Frank Force 在游戏行业工作了20年,参与过9款主流游戏、47个独立游戏的设计。在听到这个消息后,他马上和其他开发朋友讨论了这个问题,并决定做点什么为此纪念。

在此期间,他们受到三重因素的启发。一是赛车游戏,包括怀旧向的80年代赛车游戏,他们在非常早期的硬件上推动实时 3D 图形,所以作者沿用了相同的技术,用纯 JavaScript 从头开始实现做 3D 图形和物理引擎;还有一些现代赛车游戏带来了视觉设计的灵感,比如《Distance》和《Lonely Mountains: Downhill》;二是之前 Jake Gordon 用 JavaScript 创建一个虚拟3D赛车的项目,并分享了代码;三是 Chris Glover 曾经做过一款小到只有 1KB 的 JS1k 赛车游戏《Moto1kross by Chris Glover》。

于是 Frank 和他的朋友们决定做一个压缩后只有 2KB 的 3D 赛车游戏。2KB 到底有多小呢?提供一个参考,一个3.5英寸软盘可以容纳700多个这样的游戏。

他给这个游戏取名 Hue Jumper。关于名字的由来,Frank 表示,游戏的核心操作是移动。当玩家通过一个关卡时,游戏世界就会换一个颜色色调。“在我想象中,每通过过一个关卡,玩家都会跳转到另一个维度,有着完全不同的色调。”

做完这个游戏后,Frank 将包含了游戏的全部 JavaScript 代码都发布在他的个人博客上,其中用到的软件主要也是免费或开源软件的。游戏代码发布在CodePen,可以在 iframe 中试玩,有兴趣的朋友可以去看看。

以下是原博内容,AI源创评论进行了不改变原意的编译:

确定最高目标

因为严格的大小限制,我需要非常仔细对待我的程序。我的总体策略是尽可能保持一切简单,为最终目标服务。

为了帮助压缩代码,我使用了 Google Closure Compiler,它删除了所有空格,将变量重命名为1个字母字符,并进行了一些轻量级优化。

用户可以通过 Google Closure Compiler 官网在线跑代码。不幸的是,Closure Compiler 做了一些没有帮助的事情,比如替换模板字符串、默认参数和其他帮助节省空间的ES6特性。所以我需要手动撤销其中一些事情,并执行一些更“危险”的压缩技术来挤出最后一个字节空间。在压缩方面,这不算很成功,大部分挤出的空间来自代码本身的结构优化。

代码需要压缩到2KB。如果不是非要这么做不可,有一个类似的但功能没那么强的工具叫做 RegPack 。

无论哪种方式,策略都是一样的:尽最大可能重复代码,然后用压缩工具压缩。最好的例子是 c.width,c.height和 Math。因此,在阅读这段代码时,请记住,你经常会看到我不断重复一些东西,最终目的就是为了压缩。

HTML

其实我的游戏很少使用 html ,因为它主要用到的是 JavaScript 。但这是创建全屏画布 Canvas ,也能将画布 Canvas 设为窗口内部大小的代码最小方法。我不知道为什么在 CodePen 上有必要添加 overflow:hiddento the body,当直接打开时按理说也可以运行。

我将 JavaScript 封装在一个 onload 调用,得到了一个更小的最终版本… 但是,在开发过程中,我不喜欢用这个压缩设置,因为代码存储在一个字符串中,所以编辑器不能正确地高亮显示语法。

常量

有许多常量在各方面控制着游戏。当代码被 Google Closure 这样的工具缩小时,这些常量将被替换,就像 C++ 中的 #define 一样,把它们放在第一位会加快游戏微调的过程。

// draw settings

const context = c.getContext`2d`; // canvas context

const drawDistance = 800; // how far ahead to draw

const cameraDepth = 1; // FOV of camera

const segmentLength = 100; // length of each road segment

const roadWidth = 500; // how wide is road

const curbWidth = 150; // with of warning track

const dashLineWidth = 9; // width of the dashed line

const maxPlayerX = 2e3; // limit player offset

const mountainCount = 30; // how many mountains are there

const timeDelta = 1/60; // inverse frame rate

const PI = Math.PI; // shorthand for Math.PI

// player settings

const height = 150; // high of player above ground

const maxSpeed = 300; // limit max player speed

const playerAccel = 1; // player forward acceleration

const playerBrake = -3; // player breaking acceleration

const turnControl = .2; // player turning rate

const jumpAccel = 25; // z speed added for jump

const springConstant = .01; // spring players pitch

const collisionSlow = .1; // slow down from collisions

const pitchLerp = .1; // rate camera pitch changes

const pitchSpringDamp = .9; // dampen the pitch spring

const elasticity = 1.2; // bounce elasticity

const centrifugal = .002; // how much turns pull player

const forwardDamp = .999; // dampen player z speed

const lateralDamp = .7; // dampen player x speed

const offRoadDamp = .98; // more damping when off road

const gravity = -1; // gravity to apply in y axis

const cameraTurnScale = 2; // how much to rotate camera

const worldRotateScale = .00005; // how much to rotate world

// level settings

const maxTime = 20; // time to start

const checkPointTime = 10; // add time at checkpoints

const checkPointDistance = 1e5; // how far between checkpoints

const maxDifficultySegment = 9e3; // how far until max difficulty

const roadEnd = 1e4; // how far until end of road

鼠标控制

鼠标是唯一的输入系统。通过这段代码,我们可以跟踪鼠标点击和光标位置,位置显示为-1到1之间的值。

双击是通过 mouseUpFrames 实现的。mousePressed 变量只在玩家第一次点击开始游戏时使用这么一次。

mouseDown =

mousePressed =

mouseUpFrames =

mouseX = 0;

onmouseup =e=> mouseDown = 0;

onmousedown =e=> mousePressed ? mouseDown = 1 : mousePressed = 1;

onmousemove =e=> mouseX = e.x/window.innerWidth*2 - 1;

数学函数

这个游戏使用了一些函数来简化代码和减少重复,一些标准的数学函数用于 Clamp 和 Lerp 值。 ClampAngle 是有用的,因为它在 -PI 和 PI 之间 wrap angles,在许多游戏中已经广泛应用。

R函数就像个魔术师,因为它生成随机数,通过取当前随机数种子的正弦,乘以一个大数字,然后看分数部分来实现的。其实有很多方法可以做到,但这是最小的方法之一,而且对我们来说也是足够随机。

我们将使用这个随机生成器来创建各种程序,且不需要保存任何数据。例如,山脉、岩石和树木的变化不用存到内存。在这种情况下,目标不是减少内存,而是去除存储和检索数据所需的代码。

因为这是一个“真正的3D”游戏,所以有一个 3D vector class 非常有用,它也能减少代码量。这个 class 只包含这个游戏必需的基本元素,一个带有加法和乘法函数的 constructor 可以接受标量或向量参数。为了确定标量是否被传入,我们只需检查它是否小于一个大数。更正确的方法是使用 isNan 或者检查它的类型是否是 Vec3,但是这需要更多的存储。

Clamp =(v, a, b) => Math.min(Math.max(v, a), b);

ClampAngle=(a) => (a+PI) % (2*PI) + (a+PILerp =(p, a, b) => a + Clamp(p, 0, 1) * (b-a);

R =(a=1, b=0) => Lerp((Math.sin(++randSeed)+1)*1e5%1,a,b);

class Vec3 // 3d vector class

{

constructor(x=0, y=0, z=0) {this.x = x; this.y = y; this.z = z;}

Add=(v)=>(

v = v new Vec3( this.x + v.x, this.y + v.y, this.z + v.z ));

Multiply=(v)=>(

v = v new Vec3( this.x * v.x, this.y * v.y, this.z * v.z ));

}

Render Functions渲染函数

LSHA 通过模板字符串生成一组标准的 HSLA (色调、饱和度、亮度、alpha)颜色,并且刚刚被重新排序,所以更常用的 component 排在第一位。每过一关换一个整体色调也是通过这设置的。

DrawPoly 绘制一个梯形形状,用于渲染场景中的一切。使用 |0 将 Ycomponent 转换为整数,以确保每段多边形道路都能无缝连接,不然路段之间就会有一条细线。

DrawText 则用于显示时间、距离和游戏标题等文本渲染。

LSHA=(l,s=0,h=0,a=1)=>`hsl(${h+hueShift},${s}%,${l}%,${a})`;

// draw a trapazoid shaped poly

DrawPoly=(x1, y1, w1, x2, y2, w2, fillStyle)=>

{

context.beginPath(context.fillStyle = fillStyle);

context.lineTo(x1-w1, y1|0);

context.lineTo(x1+w1, y1|0);

context.lineTo(x2+w2, y2|0);

context.lineTo(x2-w2, y2|0);

context.fill;

}

// draw outlined hud text

DrawText=(text, posX)=>

{

context.font = '9em impact'; // set font size

context.fillStyle = LSHA(99,0,0,.5); // set font color

context.fillText(text, posX, 129); // fill text

context.lineWidth = 3; // line width

context.strokeText(text, posX, 129); // outline text

}

设计轨道

首先,我们必须生成完整的轨道,而且准备做到每次游戏轨道都是不同的。如何做呢?我们建立了一个道路段列表,存储道路在轨道上每一关卡的位置和宽度。轨道生成器是非常基础的操作,不同频率、振幅和宽度的道路都会逐渐变窄,沿着跑道的距离决定这一段路有多难。

atan2 函数可以用来计算道路俯仰角,据此来设计物理运动和光线。

roadGenLengthMax = // end of section

roadGenLength = // distance left

roadGenTaper = // length of taper

roadGenFreqX = // X wave frequency

roadGenFreqY = // Y wave frequency

roadGenScaleX = // X wave amplitude

roadGenScaleY = 0; // Y wave amplitude

roadGenWidth = roadWidth; // starting road width

startRandSeed = randSeed = Date.now; // set random seed

road = ; // clear road

// generate the road

for( i = 0; i {

if (roadGenLength++ > roadGenLengthMax) // is end of section?

{

// calculate difficulty percent

d = Math.min(1, i/maxDifficultySegment);

// randomize road settings

roadGenWidth = roadWidth*R(1-d*.7,3-2*d); // road width

roadGenFreqX = R(Lerp(d,.01,.02)); // X curves

roadGenFreqY = R(Lerp(d,.01,.03)); // Y bumps

roadGenScaleX = i>roadEnd ? 0 : R(Lerp(d,.2,.6));// X scale

roadGenScaleY = R(Lerp(d,1e3,2e3)); // Y scale

// apply taper and move back

roadGenTaper = R(99, 1e3)|0; // random taper

roadGenLengthMax = roadGenTaper + R(99,1e3); // random length

roadGenLength = 0; // reset length

i -= roadGenTaper; // subtract taper

}

// make a wavy road

x = Math.sin(i*roadGenFreqX) * roadGenScaleX;

y = Math.sin(i*roadGenFreqY) * roadGenScaleY;

road[i] = road[i]? road[i] : {x:x, y:y, w:roadGenWidth};

// apply taper from last section and lerp values

p = Clamp(roadGenLength / roadGenTaper, 0, 1);

road[i].x = Lerp(p, road[i].x, x);

road[i].y = Lerp(p, road[i].y, y);

road[i].w = i > roadEnd ? 0 : Lerp(p, road[i].w, roadGenWidth);

// calculate road pitch angle

road[i].a = road[i-1] ?

Math.atan2(road[i-1].y-road[i].y, segmentLength) : 0;

}

启动游戏

现在跑道就绪,我们只需要预置一些变量就可以开始游戏了。

// reset everything

velocity = new Vec3

( pitchSpring = pitchSpringSpeed = pitchRoad = hueShift = 0 );

position = new Vec3(0, height); // set player start pos

nextCheckPoint = checkPointDistance; // init next checkpoint

time = maxTime; // set the start time

heading = randSeed; // random world heading

更新玩家

这是主要的更新功能,用来更新和渲染游戏中的一切!一般来说,如果你的代码中有一个很大的函数,这不是好事,为了更简洁易懂,我们会把它分几个成子函数。

首先,我们需要得到一些玩家所在位置的道路信息。为了使物理和渲染感觉平滑,需要在当前和下一个路段之间插入一些数值。

玩家的位置和速度是 3D 向量,并受重力、dampening 和其他因素等影响更新。如果玩家跑在地面上时,会受到加速度影响;当他离开这段路时,摄像机还会抖动。另外,在对游戏测试后,我决定让玩家在空中时仍然可以跑。

接下来要处理输入指令,涉及加速、刹车、跳跃和转弯等操作。双击通过 mouseUpFrames 测试。还有一些代码是来跟踪玩家在空中停留了多少帧,如果时间很短,游戏允许玩家还可以跳跃。

当玩家加速、刹车和跳跃时,我通过spring system展示相机的俯仰角以给玩家动态运动的感觉。此外,当玩家驾车翻越山丘或跳跃时,相机还会随着道路倾斜而倾斜。

Update==>

{

// get player road segment

s = position.z / segmentLength | 0; // current road segment

p = position.z / segmentLength % 1; // percent along segment

// get lerped values between last and current road segment

roadX = Lerp(p, road[s].x, road[s+1].x);

roadY = Lerp(p, road[s].y, road[s+1].y) + height;

roadA = Lerp(p, road[s].a, road[s+1].a);

// update player velocity

lastVelocity = velocity.Add(0);

velocity.y += gravity;

velocity.x *= lateralDamp;

velocity.z = Math.max(0, time?forwardDamp*velocity.z:0);

// add velocity to position

position = position.Add(velocity);

// limit player x position (how far off road)

position.x = Clamp(position.x, -maxPlayerX, maxPlayerX);

// check if on ground

if (position.y {

position.y = roadY; // match y to ground plane

airFrame = 0; // reset air frames

// get the dot product of the ground normal and the velocity

dp = Math.cos(roadA)*velocity.y + Math.sin(roadA)*velocity.z;

// bounce velocity against ground normal

velocity = new Vec3(0, Math.cos(roadA), Math.sin(roadA))

.Multiply(-elasticity * dp).Add(velocity);

// apply player brake and accel

velocity.z +=

mouseDown? playerBrake :

Lerp(velocity.z/maxSpeed, mousePressed*playerAccel, 0);

// check if off road

if (Math.abs(position.x) > road[s].w)

{

velocity.z *= offRoadDamp; // slow down

pitchSpring += Math.sin(position.z/99)**4/99; // rumble

}

}

// update player turning and apply centrifugal force

turn = Lerp(velocity.z/maxSpeed, mouseX * turnControl, 0);

velocity.x +=

velocity.z * turn -

velocity.z ** 2 * centrifugal * roadX;

// update jump

if (airFrame++ && mouseDown && mouseUpFrames && mouseUpFrames{

velocity.y += jumpAccel; // apply jump velocity

airFrame = 9; // prevent jumping again

}

mouseUpFrames = mouseDown? 0 : mouseUpFrames+1;

// pitch down with vertical velocity when in air

airPercent = (position.y-roadY) / 99;

pitchSpringSpeed += Lerp(airPercent, 0, velocity.y/4e4);

// update player pitch spring

pitchSpringSpeed += (velocity.z - lastVelocity.z)/2e3;

pitchSpringSpeed -= pitchSpring * springConstant;

pitchSpringSpeed *= pitchSpringDamp;

pitchSpring += pitchSpringSpeed;

pitchRoad = Lerp(pitchLerp, pitchRoad, Lerp(airPercent,-roadA,0));

playerPitch = pitchSpring + pitchRoad;

// update heading

heading = ClampAngle(heading + velocity.z*roadX*worldRotateScale);

cameraHeading = turn * cameraTurnScale;

// was checkpoint crossed?

if (position.z > nextCheckPoint)

{

time += checkPointTime; // add more time

nextCheckPoint += checkPointDistance; // set next checkpoint

hueShift += 36; // shift hue

}

预渲染

在渲染之前,canvas 每当高度或宽度被重设时,画布内容就会被清空。这也适用于自适应窗口的画布。

我们还计算了将世界点转换到画布的投影比例。cameraDepth 值代表摄像机的视场(FOV)。这个游戏是90度。计算结果是 1/Math.tan(fovRadians/2) ,FOV 是90度的时候,计算结果正好是1。另外为了保持屏幕长宽比,投影按 c.width 缩放。

// clear the screen and set size

c.width = window.innerWidth, c.height = window.innerHeight;

// calculate projection scale, flip y

projectScale = (new Vec3(1,-1,1)).Multiply(c.width/2/cameraDepth);

给世界画上天空、太阳和月亮

空气背景是用全屏的 linear gradient (径向渐变)绘制的,它还会根据太阳的位置改变颜色。

为了节省存储空间,太阳和月亮在同一个循环中,使用了一个带有透明度的全屏 radial gradient(线性渐变)。

线性和径向渐变相结合,形成一个完全包围场景的天空背景。

// get horizon, offset, and light amount

horizon = c.height/2 - Math.tan(playerPitch)*projectScale.y;

backgroundOffset = Math.sin(cameraHeading)/2;

light = Math.cos(heading);

// create linear gradient for sky

g = context.createLinearGradient(0,horizon-c.height/2,0,horizon);

g.addColorStop(0,LSHA(39+light*25,49+light*19,230-light*19));

g.addColorStop(1,LSHA(5,79,250-light*9));

// draw sky as full screen poly

DrawPoly(c.width/2,0,c.width/2,c.width/2,c.height,c.width/2,g);

// draw sun and moon (0=sun, 1=moon)

for( i = 2 ; i--; )

{

// create radial gradient

g = context.createRadialGradient(

x = c.width*(.5+Lerp(

(heading/PI/2+.5+i/2)%1,

4, -4)-backgroundOffset),

y = horizon - c.width/5,

c.width/25,

x, y, i?c.width/23:c.width);

g.addColorStop(0, LSHA(i?70:99));

g.addColorStop(1, LSHA(0,0,0,0));

// draw full screen poly

DrawPoly(c.width/2,0,c.width/2,c.width/2,c.height,c.width/2,g);

}

给世界画上山峰、地平线

山脉是通过在地平线上画50个三角形,然后根据程序自己生成的。

因为用了光线照明,山脉在面对太阳时会更暗,因为它们处于阴影中。此外,越近的山脉颜色越暗,我想以此来模拟雾气。这里我有个诀窍,就是微调大小和颜色的随机值。

背景的最后一部分是绘制地平线,再用纯绿填充画布的底部。

// set random seed for mountains

randSeed = startRandSeed;

// draw mountains

for( i = mountainCount; i--; )

{

angle = ClampAngle(heading+R(19));

light = Math.cos(angle-heading);

DrawPoly(

x = c.width*(.5+Lerp(angle/PI/2+.5,4,-4)-backgroundOffset),

y = horizon,

w = R(.2,.8)**2*c.width/2,

x + w*R(-.5,.5),

y - R(.5,.8)*w, 0,

LSHA(R(15,25)+i/3-light*9, i/2+R(19), R(220,230)));

}

// draw horizon

DrawPoly(

c.width/2, horizon, c.width/2, c.width/2, c.height, c.width/2,

LSHA(25, 30, 95));

将路段投影到画布空间

在渲染道路之前,我们必须首先获得投影的道路点。第一部分有点棘手,因为我们的道路的 x 值需要转换成世界空间位置。为了使道路看起来蜿蜒曲折,我们把x值作为二阶导数。这就是为什么有奇怪的代码“x+=w+=”出现的原因。由于这种工作方式,路段没有固定的世界空间位置,每一帧都是根据玩家的位置重新计算。

一旦我们有了世界空间位置,我们就可以从道路位置中知道玩家的位置,从而得到本地摄像机空间位置。代码的其余部分,首先通过旋转标题、俯仰角来应用变换,然后通过投影变换,做到近大远小的效果,最后将其移动到画布空间。

for( x = w = i = 0; i {

p = new Vec3(x+=w+=road[s+i].x, // sum local road offsets

road[s+i].y, (s+i)*segmentLength) // road y and z pos

.Add(position.Multiply(-1)); // get local camera space

// apply camera heading

p.x = p.x*Math.cos(cameraHeading) - p.z*Math.sin(cameraHeading);

// tilt camera pitch and invert z

z = 1/(p.z*Math.cos(playerPitch) - p.y*Math.sin(playerPitch));

p.y = p.y*Math.cos(playerPitch) - p.z*Math.sin(playerPitch);

p.z = z;

// project road segment to canvas space

road[s+i++].p = // projected road point

p.Multiply(new Vec3(z, z, 1)) // projection

.Multiply(projectScale) // scale

.Add(new Vec3(c.width/2,c.height/2)); // center on canvas

}

绘制路段

现在我们有了每个路段的画布空间点,渲染就相当简单了。我们需要从后向前画出每一个路段,或者更具体地说,连接上一路段的梯形多边形。

为了创建道路,这里有4层渲染:地面,条纹路边缘,道路本身和白色虚线。每一个都是基于路段的俯仰角和方向来加阴影,并且根据该层的表现还有一些额外的逻辑。

有必要检查路段是在近还是远剪辑范围,以防止渲染出现 bug 。此外,还有一个很好的优化方法是,当道路变得很窄时,可以通过 distance 来减小道路的分辨率。如此,不仅减少了 draw count 一半以上,而且没有明显的质量损失,这是一次性能胜利。

let segment2 = road[s+drawDistance]; // store the last segment

for( i = drawDistance; i--; ) // iterate in reverse

{

// get projected road points

segment1 = road[s+i];

p1 = segment1.p;

p2 = segment2.p;

// random seed and lighting

randSeed = startRandSeed + s + i;

light = Math.sin(segment1.a) * Math.cos(heading) * 99;

// check near and far clip

if (p1.z 0)

{

// fade in road resolution over distance

if (i % (Lerp(i/drawDistance,1,9)|0) == 0)

{

// ground

DrawPoly(c.width/2, p1.y, c.width/2,

c.width/2, p2.y, c.width/2,

LSHA(25 + light, 30, 95));

// curb if wide enough

if (segment1.w > 400)

DrawPoly(p1.x, p1.y, p1.z*(segment1.w+curbWidth),

p2.x, p2.y, p2.z*(segment2.w+curbWidth),

LSHA(((s+i)%19

// road and checkpoint marker

DrawPoly(p1.x, p1.y, p1.z*segment1.w,

p2.x, p2.y, p2.z*segment2.w,

LSHA(((s+i)*segmentLength%checkPointDistance 70 : 7) + light));

// dashed lines if wide and close enough

if ((segment1.w > 300) && (s+i)%9==0 && i DrawPoly(p1.x, p1.y, p1.z*dashLineWidth,

p2.x, p2.y, p2.z*dashLineWidth,

LSHA(70 + light));

// save this segment

segment2 = segment1;

}

绘制路边的树和石头

游戏有两种不同类型的物体:树和石头。首先,我们通过使用 R 函数来确定是否加一个对象。这是随机数和随机数种子特别有意思的地方。我们还将使用 R 为对象随机添加不同的形状和颜色。

最初我还想涉及其他车型,但为了达到 2KB 的要求,必须要进行特别多的削减,因此我最后放弃了这个想法,用风景作为障碍。这些位置是随机的,也比较靠近道路,不然它们太稀疏,就很容易行驶。为了节省空间,对象高度还决定了对象的类型。

这是通过比较玩家和物体在 3D 空间中的位置来检查它们之间的碰撞位置。当玩家撞到一个物体时,玩家减速,该物体被标记为“ hit ”,这样它就可以安全通过。

为了防止对象突然出现在地平线上,透明度会随着距离的接近而削弱。梯形绘图函数定义物体的形状和颜色,另外随机函数会改变这两个属性。

if (R
{
// player object collision check
x = 2*roadWidth * R(10,-10) * R(9); // choose object pos
const objectHeight = (R(2)|0) * 400; // choose tree or rock
if (!segment1.h // dont hit same object
&& Math.abs(position.x-x) && Math.abs(position.z-(s+i)*segmentLength) && position.y-height {
// slow player and mark object as hit
velocity = velocity.Multiply(segment1.h = collisionSlow);
}

// draw road object
const alpha = Lerp(i/drawDistance, 4, 0); // fade in object
if (objectHeight)
{
// tree trunk
DrawPoly(x = p1.x+p1.z * x, p1.y, p1.z*29,
x, p1.y-99*p1.z, p1.z*29,
LSHA(5+R(9), 50+R(9), 29+R(9), alpha));

// tree leaves
DrawPoly(x, p1.y-R(50,99)*p1.z, p1.z*R(199,250),
x, p1.y-R(600,800)*p1.z, 0,
LSHA(25+R(9), 80+R(9), 9+R(29), alpha));
}
else
{
// rock
DrawPoly(x = p1.x+p1.z*x, p1.y, p1.z*R(200,250),
x+p1.z*(R(99,-99)), p1.y-R(200,250)*p1.z, p1.z*R(99),
LSHA(50+R(19), 25+R(19), 209+R(9), alpha));
}
}
}
}

画上 HUD,更新时间,请求下一次更新

游戏的标题、时间和距离是用一个非常基础的字体渲染系统显示出来的,就是之前设置的 DrawText 函数。在玩家点击鼠标之前,它会在屏幕中央显示标题。

按下鼠标后,游戏开始,然后 HUD 会显示剩余时间和当前距离。时间也在这块更新,玩过此类游戏的都知道,时间只在比赛开始后减少。

在这个 massive Update function 结束后,它调用 requestAnimationFrame (Update) 来触发下一次更新。

if (mousePressed)

{

time = Clamp(time - timeDelta, 0, maxTime); // update time

DrawText(Math.ceil(time), 9); // show time

context.textAlign = 'right'; // right alignment

DrawText(0|position.z/1e3, c.width-9); // show distance

}

else

{

context.textAlign = 'center'; // center alignment

DrawText('HUE JUMPER', c.width/2); // draw title text

}

requestAnimationFrame(Update); // kick off next frame

} // end of update function

代码的最后一位

HTML 需要一个结束脚本标签来让所有的代码能够跑起来。

Update; // kick off update loop

压缩

这就是整个游戏啦!下方的一小段代码就是压缩后的最终结果,我用不同的颜色标注了不同的部分。完成所有这些工作后,你能感受到我在2KB内就做完了整个游戏是多么让我满意了吗?而这还是在zip之前的工作,zip还可以进一步压缩大小。

警告 Caveats

当然,还有很多其他 3D 渲染方法可以同时保证性能和视觉效果。如果我有更多的可用空间,我会更倾向于使用一个 WebGL API 比如 three.js ,我在去年制作的一个类似游戏“Bogus Roads”中用过这个框架。此外,因为它使用的是 requestAnimationFrame ,所以需要一些额外的代码来确保帧速率不超过60 fps,增强版本中我会这么用,尽管我更喜欢使用 requestAnimationFrame 而不是 setInterval ,因为它是垂直同期的(VSyn,VerticalSynchronization),所以渲染更丝滑。这种代码的一个主要好处是它非常兼容,可以在任何设备上运行,尽管在我旧 iPhone 上运行有点慢。

游戏代码被我放到了 GitHub 上的 GPL-3.0 下(https://github.com/KilledByAPixel/HueJumper2k),所以你可以在自己的项目中自由使用它。该库中还包含 2KB 版本的游戏,准确说是2031字节!欢迎你添加一些其他的功能,比如音乐和音效到“增强”版本中。

后记

雷锋网

信IDWEB_wysj(点击关注) ◎ ◎ ◎ ◎ ◎◎◎◎◎一┳═┻︻▄

(页底留言开放,欢迎来吐槽)

● ● ●

要玩转css3的3d,就必须了解几个词汇,便是透视(perspective)、旋转(rotate)和移动(translate)。透视即是以现实的视角来看屏幕上的2D事物,从而展现3D的效果。旋转则不再是2D平面上的旋转,而是三维坐标系的旋转,就包括X轴,Y轴,Z轴旋转。平移同理。

当然用理论来说明,估计你还不明白。下面是3个gif:

沿着X轴旋转

沿着Y轴旋转

沿着Z轴旋转

旋转应该没问题了,那理解平移起来就比较容易了,就是在在X轴、Y轴、z轴移动。

你可能会说透视比较不好理解,下面我介绍一下透视的几个属性。

perspective

perspective英文名便是透视,没有这东西就没办法形成3D效果,但是这个东西是怎么让我们浏览器形成3D 效果的呢,可以看下面这张图,其实学过绘画的应该知道透视关系,而这里就是这个道理。

但是在css里它是有数值的,例如perspective: 1000px这个代表什么呢?我们可以这样理解,如果我们直接眼睛靠着物体看,物体就超大占满我们的视线,我们离它距离越来越大,它在变小,立体感也就出来了是不是,其实这个数值构造了一个我们眼睛离屏幕的距离,也就构造了一个虚拟3D假象。

perspective-origin

由上面我们了解了perspective,而加上了这个origin是什么,我们前面说的这个是眼睛离物体的距离,而这个就是眼睛的视线,我们的视点的不同位置就决定了我们看到的不同景象,默认是中心,为perspectice-origin: 50% 50%,第一个数值是 3D 元素所基于的 X 轴,第二个定义在 y 轴上的位置

当为元素定义 perspective-origin 属性时,其子元素会获得透视效果,而不是元素本身。必须与 perspective 属性一同使用,而且只影响 3D 转换元素。(W3school)

transform-style

perspective又来了,没错,它是css中3D的关键,transform-style默认是flat,如果你要在元素上视线3D效果的话,就必须用上transform-style: preserve-3d,否则就只是平面的变换,而不是3D的变换

手把手带你玩转css3-3d

以上我们稍微了解概念,下面就开始实战吧!

我们看一个效果,是不是很酷炫~

第一步:html结构

很简单的一个容器包裹着一个装了6个piecepiece-box

<div class="container"> <div class="piece-box"> <div class="piece piece-1"></div> <div class="piece piece-2"></div> <div class="piece piece-3"></div> <div class="piece piece-4"></div> <div class="piece piece-5"></div> <div class="piece piece-6"></div> </div></div>

第二步: 加上必要的3D属性,进入3D世界

通过上面的讲解,应该对perspective比较熟悉了吧,

/*容器*/.container { -webkit-perspective: 1000px; -moz-perspective: 1000px; -ms-perspective: 1000px; perspective: 1000px; }/*piece盒子*/ .piece-box { perspective-origin: 50% 50%; -webkit-transform-style: preserve-3d; -moz-transform-style: preserve-3d; -ms-transform-style: preserve-3d; transform-style: preserve-3d; }

第三步:加入必要的样式

/*容器*/.container { -webkit-perspective: 1000px; -moz-perspective: 1000px; -ms-perspective: 1000px; perspective: 1000px; }/*piece盒子*/.piece-box { position: relative; width: 200px; height: 200px; margin: 300px auto; perspective-origin: 50% 50%; -webkit-transform-style: preserve-3d; -moz-transform-style: preserve-3d; -ms-transform-style: preserve-3d; transform-style: preserve-3d; }/*piece通用样式*/.piece { position: absolute; width: 200px; height: 200px; background: red; opacity: 0.5; }.piece-1 { background: #FF6666; }.piece-2 { background: #FFFF00; }.piece-3 { background: #006699; }.piece-4 { background: #009999; }.piece-5 { background: #FF0033; }.piece-6 { background: #FF6600; }

当然,在你完成这步之后你还是只看到一个正方形,也就是我们的piece-6,因为我们的3Dtransform还没开始

第四步:3D变换来袭

首先是实现走马灯,我们先不要让它走,先实现灯部分。

如何实现呢?因为要构成一个圆,圆是360度,而我们有6个piece,那么,很容易想到,我们需要把每一个piece以递增60度的方式rotateY,就变成如下

如何把他们从中心拉开呢?

这里我们还要注意一点,在我们使元素绕Y轴旋转以后,其实X轴和Z轴也会跟着旋转,所所以正方体的每个面的垂直线还是Z轴,我们就只需要改变下translateZ的值,而当translateZ为正的时候,就朝着我们的方向走来,这样就可以拉开了

但是拉开的距离如何衡量?

是不是一目了然了~

下面我们再修改下css

.piece-1 { background: #FF6666; -webkit-transform: rotateY(0deg) translateZ(173.2px); -ms-transform: rotateY(0deg) translateZ(173.2px); -o-transform: rotateY(0deg) translateZ(173.2px); transform: rotateY(0deg) translateZ(173.2px); }.piece-2 { background: #FFFF00; -webkit-transform: rotateY(60deg) translateZ(173.2px); -ms-transform: rotateY(60deg) translateZ(173.2px); -o-transform: rotateY(60deg) translateZ(173.2px); transform: rotateY(60deg) translateZ(173.2px); }.piece-3 { background: #006699; -webkit-transform: rotateY(120deg) translateZ(173.2px); -ms-transform: rotateY(120deg) translateZ(173.2px); -o-transform: rotateY(120deg) translateZ(173.2px); transform: rotateY(120deg) translateZ(173.2px); }.piece-4 { background: #009999; -webkit-transform: rotateY(180deg) translateZ(173.2px); -ms-transform: rotateY(180deg) translateZ(173.2px); -o-transform: rotateY(180deg) translateZ(173.2px); transform: rotateY(180deg) translateZ(173.2px); }.piece-5 { background: #FF0033; -webkit-transform: rotateY(240deg) translateZ(173.2px); -ms-transform: rotateY(240deg) translateZ(173.2px); -o-transform: rotateY(240deg) translateZ(173.2px); transform: rotateY(240deg) translateZ(173.2px); }.piece-6 { background: #FF6600; -webkit-transform: rotateY(300deg) translateZ(173.2px); -ms-transform: rotateY(300deg) translateZ(173.2px); -o-transform: rotateY(300deg) translateZ(173.2px); transform: rotateY(300deg) translateZ(173.2px); }

是不是已经实现了走马灯了?

第五步:animation让3D动起来

要实现走马灯动,其实很简单,我们只要在piece-box上加上旋转动画就行了,5s完成动画,从0度旋转到360度

/*piece盒子*/.piece-box { position: relative; width: 200px; height: 200px; margin: 300px auto; perspective-origin: 50% 50%; -webkit-transform-style: preserve-3d; -moz-transform-style: preserve-3d; -ms-transform-style: preserve-3d; transform-style: preserve-3d; animation: pieceRotate 5s; -moz-animation: pieceRotate 5s; /* Firefox */ -webkit-animation: pieceRotate 5s; /* Safari and Chrome */ -o-animation: pieceRotate 5s ; /* Opera */}/*走马灯动画*/@keyframes pieceRotate { 0% {-webkit-transform: rotateY(0deg); -ms-transform: rotateY(0deg); -o-transform: rotateY(0deg); transform: rotateY(0deg);} 100% {-webkit-transform: rotateY(360deg); -ms-transform: rotateY(360deg); -o-transform: rotateY(360deg); transform: rotateY(360deg);} }/* Firefox */@-moz-keyframes pieceRotate { 0% {-webkit-transform: rotateY(0deg); -ms-transform: rotateY(0deg); -o-transform: rotateY(0deg); transform: rotateY(0deg);} 100% {-webkit-transform: rotateY(360deg); -ms-transform: rotateY(360deg); -o-transform: rotateY(360deg); transform: rotateY(360deg);} }/* Safari and Chrome */@-webkit-keyframes pieceRotate { 0% {-webkit-transform: rotateY(0deg); -ms-transform: rotateY(0deg); -o-transform: rotateY(0deg); transform: rotateY(0deg);} 100% {-webkit-transform: rotateY(360deg); -ms-transform: rotateY(360deg); -o-transform: rotateY(360deg); transform: rotateY(360deg);} }/* Opera */@-o-keyframes pieceRotate { 0% {-webkit-transform: rotateY(0deg); -ms-transform: rotateY(0deg); -o-transform: rotateY(0deg); transform: rotateY(0deg);} 100% {-webkit-transform: rotateY(360deg); -ms-transform: rotateY(360deg); -o-transform: rotateY(360deg); transform: rotateY(360deg);} }

这里就不放gif了~hhh是不是实现了酷炫的效果,还没结束哦~更酷炫的正方体组装

正方体,其实也不难实现,我这里就不很详细说了,你首先可以想象一个面,然后去拓展其他面如何去实现,比如我们把正方体的前面translateZ(100px)让它靠近我们100px,然后后面translateZ(-100px)让它远离我们100px,左边是先translateX(-100pxrotateY(90deg),右边就是translateX(100px)rotateY(90deg),上面是先translateY(-100px)rotateX(90deg),下面是先translateY(100px)rotateX(90deg),只要我们写进动画,一切就大功告成。

css就为如下,以下就只放piece1,其他读者可以自己类比实现,或者看我github的源码

.piece-1 { background: #FF6666; -webkit-transform: rotateY(0deg) translateZ(173.2px); -ms-transform: rotateY(0deg) translateZ(173.2px); -o-transform: rotateY(0deg) translateZ(173.2px); transform: rotateY(0deg) translateZ(173.2px); animation: piece1Rotate 5s 5s; -moz-animation: piece1Rotate 5s 5s; /* Firefox */ -webkit-animation: piece1Rotate 5s 5s; /* Safari and Chrome */ -o-animation: piece1Rotate 5s 5s; /* Opera */ -webkit-animation-fill-mode: forwards; /* Chrome, Safari, Opera */ animation-fill-mode: forwards; }/*front*/ @keyframes piece1Rotate { 0% {-webkit-transform: rotateY(0deg) translateZ(173.2px); -ms-transform: rotateY(0deg) translateZ(173.2px); -o-transform: rotateY(0deg) translateZ(173.2px); transform: rotateY(0deg) translateZ(173.2px);} 100% {-webkit-transform: rotateY(0deg) translateZ(100px); -ms-transform: rotateY(0deg) translateZ(100px); -o-transform: rotateY(0deg) translateZ(100px); transform: rotateY(0deg) translateZ(100px);} } /* Firefox */ @-moz-keyframes piece1Rotate { 0% {-webkit-transform: rotateY(0deg) translateZ(173.2px); -ms-transform: rotateY(0deg) translateZ(173.2px); -o-transform: rotateY(0deg) translateZ(173.2px); transform: rotateY(0deg) translateZ(173.2px);} 100% {-webkit-transform: rotateY(0deg) translateZ(100px); -ms-transform: rotateY(0deg) translateZ(100px); -o-transform: rotateY(0deg) translateZ(100px); transform: rotateY(0deg) translateZ(100px);} } /* Safari and Chrome */ @-webkit-keyframes piece1Rotate { 0% {-webkit-transform: rotateY(0deg) translateZ(173.2px); -ms-transform: rotateY(0deg) translateZ(173.2px); -o-transform: rotateY(0deg) translateZ(173.2px); transform: rotateY(0deg) translateZ(173.2px);} 100% {-webkit-transform: rotateY(0deg) translateZ(100px); -ms-transform: rotateY(0deg) translateZ(100px); -o-transform: rotateY(0deg) translateZ(100px); transform: rotateY(0deg) translateZ(100px);} } /* Opera */ @-o-keyframes piece1Rotate { 0% {-webkit-transform: rotateY(0deg) translateZ(173.2px); -ms-transform: rotateY(0deg) translateZ(173.2px); -o-transform: rotateY(0deg) translateZ(173.2px); transform: rotateY(0deg) translateZ(173.2px);} 100% {-webkit-transform: rotateY(0deg) translateZ(100px); -ms-transform: rotateY(0deg) translateZ(100px); -o-transform: rotateY(0deg) translateZ(100px); transform: rotateY(0deg) translateZ(100px);} }

细心的读者可以发现我用了一个animation-fill-mode: forwards;,这个其实就是让这些piece保持动画最后的效果,也就是正方体的效果,读者可以不加试试看,那还是会恢复原样。

最后就是正方体的旋转了,前面我们的容器已经用过animation了,读者可能会想我再加个class放animaiton不就行了,hhh,animaiton会覆盖掉前面的,那前面的走马灯的动画就没了,所以在html结构中,我再加了一个box包裹piece, 如下

<div class="container"> <div class="piece-box"> <div class="piece-box2"><!--新加的容器--> <div class="piece piece-1"></div> <div class="piece piece-2"></div> <div class="piece piece-3"></div> <div class="piece piece-4"></div> <div class="piece piece-5"></div> <div class="piece piece-6"></div> </div> </div></div>

在动画上我们可以控制正方体动画的延时时间,就是等到正方体组装完成后再开始动画

animation: boxRotate 5s 10s infinite;第一个5s是动画时长,第二个10s是延时时间,然后我们让正方体的旋转,绕X轴从0度到360度,绕Y轴也0到Y轴360度。

.piece-box2 { -webkit-transform-style: preserve-3d; -moz-transform-style: preserve-3d; -ms-transform-style: preserve-3d; transform-style: preserve-3d; animation: boxRotate 5s 10s infinite; -moz-animation: boxRotate 5s 10s infinite; /* Firefox */ -webkit-animation: boxRotate 5s 10s infinite; /* Safari and Chrome */ -o-animation: boxRotate 5s 10s infinite; /* Opera */}/*正方体旋转动画*/@keyframes boxRotate {0% {-webkit-transform: rotateX(0deg) rotateY(0deg);); -ms-transform: rotateX(0deg) rotateY(0deg);); -o-transform: rotateX(0deg) rotateY(0deg);); transform: rotateX(0deg) rotateY(0deg););}100% {-webkit-transform: rotateX(360deg) rotateY(360deg); -ms-transform: rotateX(360deg) rotateY(360deg); -o-transform: rotateX(360deg) rotateY(360deg); transform: rotateX(360deg) rotateY(360deg);} }/* Firefox */@-moz-keyframes boxRotate {0% {-webkit-transform: rotateX(0deg) rotateY(0deg);); -ms-transform: rotateX(0deg) rotateY(0deg);); -o-transform: rotateX(0deg) rotateY(0deg);); transform: rotateX(0deg) rotateY(0deg););}100% {-webkit-transform: rotateX(360deg) rotateY(360deg); -ms-transform: rotateX(360deg) rotateY(360deg); -o-transform: rotateX(360deg) rotateY(360deg); transform: rotateX(360deg) rotateY(360deg);} }/* Safari and Chrome */@-webkit-keyframes boxRotate {0% {-webkit-transform: rotateX(0deg) rotateY(0deg);); -ms-transform: rotateX(0deg) rotateY(0deg);); -o-transform: rotateX(0deg) rotateY(0deg);); transform: rotateX(0deg) rotateY(0deg););}100% {-webkit-transform: rotateX(360deg) rotateY(360deg); -ms-transform: rotateX(360deg) rotateY(360deg); -o-transform: rotateX(360deg) rotateY(360deg); transform: rotateX(360deg) rotateY(360deg);} }/* Opera */@-o-keyframes boxRotate {0% {-webkit-transform: rotateX(0deg) rotateY(0deg);); -ms-transform: rotateX(0deg) rotateY(0deg);); -o-transform: rotateX(0deg) rotateY(0deg);); transform: rotateX(0deg) rotateY(0deg););}100% {-webkit-transform: rotateX(360deg) rotateY(360deg); -ms-transform: rotateX(360deg) rotateY(360deg); -o-transform: rotateX(360deg) rotateY(360deg); transform: rotateX(360deg) rotateY(360deg);} }

最后效果,大功告成!

干货!免费领取腾讯高级讲师网页设计教程


点我领取

☝☝☝

关注网页设计自学平台,99%的努力都在这里

▼想结交前端大神的可以点「阅读原文」噢!

:点击上方"蓝色字体"↑ 可以订阅噢!

摘要 51RGB官方微信

篇 CSS 3D 的文章,其实酝酿已久,从CSS 3D 出来的时候就已经在关注,只是要写 CSS 3D 真的很费工,里面有太多东西要讲,加上最近在做 Webduino 可以改变世界的事业 ( Webduino 超赞呀! ),所以就一直搁着了,趁着端午放假,一口气把它搞定吧!

虽然 CSS 3D 并非真的 3D,CSS 3D 坦白说就是纯粹利用计算的方法,借着浏览器的高效性能,在 2D 的空間绘制一个 3D 的图形,就像我们拿张约,用铅笔在上头画个正立方体之类的,也因为是借了浏览器的运算,自然而然非常地耗性能,往往只要有过多的形状转换为 3D 呈现,就会发现电脑的风扇开始狂转,这也是使用 CSS 3D 必须要注意的地方,毕竟 CSS 原本就不是拿来做 3D 应用的技术,可以画 3D,只是可以加强 CSS 呈现的美感和能力,但用在精细的 3D 动画或转场效果,还是交给专业的 3D 绘图软件来运行。

从这篇开始以及再来的一两篇,将会深入介绍 CSS 3D 的绘图以及直接做些应用,如果你已经会了 CSS 3D,不妨也可以看看正多面体该如何制作,可以参考這两篇:玩转 CSS 3D - 正四面体与正六面体、玩转 CSS 3D - 正八面体与正十二面体。

3D手式

我们这边先来用 Google SketchUp 这个免费的 3D 建模软件,看一下最普通的 3D 绘图的介面,里头最基本的会有三个元素,第一个:摄影镜头,第二个:立体空间,第三个:立体物件。

摄影镜头主要定义了观看者的角度,就如同我们在拍照,使用长焦距的望远镜头,物体可以拉近且不会变形,使用短焦距的广角镜,拍摄的物体就容易变形,从下图可以看出,镜头的焦距可以让空间內的物体产生不同的变形,至于立体空间则是具备了 XYZ三个座标轴的空间,立体物件则是在这个空间里头的物件。

所以绘制 CSS 的 3D 图形,最重要的也就是要架设这三个物件,不过因为在 CSS 里並沒有摄影镜头、立体空間...等。這些 3D 软件才有的元素,所以变成都是用 div取代,在对应的div上头加入对应的 style 属性,就可以进行模拟,运用上面所提到的三个元素,我们这里就必须要用到三层div,最外层是摄影镜头,第二层为立体空间,第三层是是立体空间內的立体元素,写出來的 HTML 长得就像下面这样:

<div class="camera"> <div class="space"> <div class="box"></div> </div></div>

设定 camera

接着就要来把最外层的 div( 以下通称 camera ) 设定为摄影镜头,设定的方法为添加perspective-origin以及perspective这两个属性,这个属性是什么呢?简单来说就是透视点以及镜头到透视点的距离,如果直接查询perspective,看到的八九不离十是下面这些图案:

然而在 W3C 里头对于 perspective的解释则是下图这样,透视点同样也是物体到摄影机的距离 (d) ,但又因为 CSS 的 3D 空间里头具有Z轴,所以perspective的距离会因为Z轴的关系而有所缩放 ( 不过千万要注意,这里的Z指的是物体的Z轴,也就是translateZ,不是摄影机的 )。

此外,perspective-origin是摄影机的中心点位置,预设相对应空间div( 以下都称为 space ) 的中心点,不做设定的话,预设都是center center( 或50% 50%),换句话说,作为摄影镜头的 camera 的三個维度,perspective-origin代表了XY轴,而perspective代表Z轴 ( 和內容物体的Z轴相减才会变成摄影机的 ),camera 就可以在三维空间里头进行移动,下图同样是 W3C 对于perspective-origin所作的解释,当摄影镜头往上,图形的下半部就看不到了。

回到 CSS 来看的话,我们可以像下面這样设定,这时候画面完全不是正常的,因为还沒有设定空间和物体。

.camera{ width:200px; height:200px; perspective-origin:center center; perspective:500px;}

设定 space

摄影机完成后,就是要设定一个立体空间 space,这个空间设定的方式很简单,只要设定一个属性:transform-style,这个属性预设为flat,也就是只要是這个div內的子元素,一律都是以扁平 (flat) 的方式呈现,所属的变换transform也一律都是用flat的方式变换,换句话说就是沒有Z轴的存在,为了让內容元素都是立体元素,所以我们要將transform-style设为3D,如此一来內容元素就全部都可以用 3D 进行变换,为了方便区分,下面我将 space 外围多加一个boder做区别。

.space{ width:100%; height:100%; border:1px dashed #000; transform-style:3d;}

设定 box

最后就是內容元素 box 了,我们可以添加一个 100px x 100px的 box 进去,接着,用这个 box 来复习一下前面讲的观念,在沒有设定 box 的traslateZrotate的情形下,不论我们如何去修改 camera 的perspective-originperspective,box 的大小和位置都不会有变化,为什么呢?因为在沒有设定 box 的translateZrotate,让Z的深度有所变化,摄影机透过perspective看出去的位置都是相同的,也造成不论怎么去看这个 box 都是一样的大小。

.box{ width:100px; height:100px; background:#069; transform:translateX(50px) translateY(50px); }

不过当我们给 box 改变 Z轴的深度之后 ( 这里我先把translateZ设定为150px),再去改变 camera 的perspective-originperspective,一切彷佛就有了变化。

.box{ width:100px; height:100px; background:#069; transform:translateX(50px) translateY(50px) translateZ(150px); translateZ(150px); translateZ(150px); }

大概了解之后,来把 box 旋转一下角度,看得应该就会更清楚,当摄影机变成广角,也就是 perspective变短,整个旋转后变形也会更加明显,大家可以用开发者工具修改 camera 的perspective就会明白。

.box{ width:100px; height:100px; background:#069; transform:translateX(50px) translateY(50px) rotateY(60deg);}

改变一下 perspective-origin也会很有意思。

我们加入多一点的 box,并且让这些 box 的位置改变或旋转,看看效果如何,这里比较需要注意的地方,是我们必须要额外在每个 box 加入 position:absolute的属性,因为div本身为 block 属性,会互相挤压,要设定位置为绝对位置,才会正确地放在 space 里头。

.space div{ position:absolute; width:100px; height:100px;}.box1{ background:#069; transform:translateX(50px) translateY(50px) rotateY(60deg);}.box2{ background:#c00; transform:translateX(100px) translateY(20px) rotateX(60deg);}.box3{ background:#f90; transform:translateX(0px) translateZ(-250px) rotateY(20deg);}.box4{ background:#0c9; transform:translateX(20px) translateY(80px) rotateX(-80deg);}

正如上述的三个 3D 元素,我们就可以轻松的绘制 CSS 3D 图形,不过除了 camera、space 和 box 之外,还有一个最重要最重要最重要的撰写規律在里头 ( 因为很重要所以要说三次 ),这个規律就是 tramsform里头是有順序的,因为 CSS 3D 完全是由 2D 演算而来,并不是真的像 3D 软件是真的有 3D 的空间,所以就变成会「按照順序」进行演算,而且又因为transform会造成物体的整个座标轴变换,在順序的编排上就格外重要。

例如今天我先让 box 在 X轴上水平位移100px再绕着Y轴順时针转60度,和先绕Y轴順时针转60度,再在X轴上头水平位移100px的结果会完全不同,因为当我先绕了Y轴转动,整个X轴也会跟著转动,这时候再做水平位移,位置就会像是在深度做变换。

.space div{ position:absolute; width:100px; height:100px;}.box1{ background:#069; transform:translateY(50px) translateX(100px) rotateY(60deg);}.box2{ background:#c00; transform:translateY(50px) rotateY(60deg) translateX(100px);}

transform的数量少还比较看不出來,当今天transform里头数量一多,造成的差异就更加明显,这也是在玩 CSS 3D 最最最最最需要注意的重点所在,一定要注意,一定要注意,一定要注意,非常重要所以再说三次呀!

.space div{ position:absolute; width:100px; height:100px;}.box1{ background:#069; transform:translateY(50px) translateX(100px) rotateY(60deg) rotateX(60deg) translateX(-50px);}.box2{ background:#c00; transform:translateX(-50px) translateY(50px) rotateX(60deg) rotateY(60deg) translateX(100px);}

以上就是 CSS 3D 的原理解析,坦白说如果明白了 3D 的结构组成,CSS 的 3D 就沒有难度,总而言之,就是先建立好三個元素:摄影机、立体空间、立体物件,就可以开始玩转 CSS 3D 啰!

想认识志同道合的朋友一起学习web

加入我们的学习QQ群 190166743

丰富的学习资源,周一到周四免费直播公开课

长按图片,识别二维码即可入群

你可能感兴趣的精彩内容

微信:UI设计自学平台加关注

长按关注:《UI设计自学平台》

↓↓↓