整合營銷服務商

          電腦端+手機端+微信端=數據同步管理

          免費咨詢熱線:

          如何用不到 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設計自學平臺》

          ↓↓↓


          主站蜘蛛池模板: 成人精品视频一区二区三区尤物| 波多野结衣av高清一区二区三区| 精品一区二区三区视频| 色狠狠AV一区二区三区| 国产精品毛片一区二区| 一区二区三区在线观看| 无码国产精品一区二区免费式芒果| 一区二区三区91| 精品视频午夜一区二区| 一本一道波多野结衣一区| 日韩高清国产一区在线| 亚洲av一综合av一区| 国产综合无码一区二区三区| 极品少妇伦理一区二区| 无码一区二区三区中文字幕| 亚洲日韩一区二区一无码| 国产美女在线一区二区三区| 老熟妇仑乱视频一区二区| 97av麻豆蜜桃一区二区| 亚洲av鲁丝一区二区三区| 日韩电影一区二区三区| 久久免费精品一区二区| 亚洲国产精品乱码一区二区| 久久精品一区二区国产| 无码少妇一区二区三区浪潮AV | 亚洲AV无码一区二区三区牲色| 中文字幕人妻无码一区二区三区| 91福利国产在线观看一区二区| 少妇人妻偷人精品一区二区| av无码人妻一区二区三区牛牛| 人妻少妇AV无码一区二区| 亚洲AV无码第一区二区三区| 国产激情з∠视频一区二区| 亚洲制服丝袜一区二区三区| 亚洲一区二区三区高清不卡| 午夜一区二区免费视频| 亚洲AV日韩精品一区二区三区| 中文字幕日韩一区二区三区不 | 日本精品一区二区久久久| 国产激情一区二区三区成人91| 亚洲一区AV无码少妇电影☆|