WebGL自學課程(8):WebGL+ArcGIS JS API實現TerrainMap
轉載請註明出處
以前在Esri的部落格上看到了一篇用Silverlight+Balder實現TerrainMap的文章,實現的功能是將指定的二維投影地理範圍轉換成三維地形圖,這是連結地址http://maps.esri.com/sldemos/terrainmap/default.html,感覺很有意思,最近在看WebGL,所以就想用WebGL重新進行實現,其中用ArcGIS JS API獲取圖片和高程資料,用WebGL進行三維顯示。由於對WebGL框架不是很熟悉,所以開始的時候就想用原生的WebGL進行開發,後來發現越寫越多,乾脆萌生了一個將這些程式碼封裝成自己的WebGL圖形庫的想法,取名World.js,我現在設定的World.js的版本是World0.3.5,World0.3.5的原始碼連結地址http://blog.csdn.net/sunqunsunqun/article/details/7885639,做這個TerrainMap的一個主要目的就是熟習WebGL,正好順便把常用的WebGL程式碼封裝成框架,以便增加程式碼的複用率。
現在這個Demo還有諸多不足,需要以後改正。
下圖是二維介面:
下面是用WebGL實現的TerrainMap:
下面是Demo的組織結構:
World0.3.5的原始碼連結地址http://blog.csdn.net/sunqunsunqun/article/details/7885639。
以下是CanvasEventHandle.js程式碼:
var bMouseDown = false;
var handleMouseMove;
var previousX=-1;
var previousY=-1;
var MODE = "ROTATE";
function onMouseDown(evt){
previousX=evt.layerX||evt.offsetX;
previousY=evt.layerY||evt.offsetY;
bMouseDown = true;
handleMouseMove = dojo.connect(dojo.byId("canvasId"),"onmousemove","onMouseMove");
}
function onMouseMove(evt){
var currentX=evt.layerX||evt.offsetX;
var currentY=evt.layerY||evt.offsetY;
if(MODE == "PAN"){
if(bMouseDown){
onPanMouseMove(currentX,currentY);
}
}
else if(MODE == "ROTATE"){
if(bMouseDown){
onRotateMouseMove(currentX,currentY);
}
}
previousX = currentX;
previousY = currentY;
}
function onRotateMouseMove(currentX,currentY){
if(previousX > 0 && previousY > 0){
var changeX = currentX - previousX;
var changeY = currentY - previousY;
var horCameraAngle = World.canvas.width / World.canvas.height * camera.fov;
var changeHorAngle = changeX / World.canvas.width * horCameraAngle;
var changeVerAngle = changeY / World.canvas.height * camera.fov;
camera.worldRotateY(-changeHorAngle*Math.PI/180);
var lightDir = camera.getLightDirection();
//if(Math.abs(lightDir.z)>0.01){
var plumbVector = getPlumbVector(lightDir,false);
camera.worldRotateByVector(-changeVerAngle*Math.PI/180,plumbVector);
//}
}
}
function onPanMouseMove(currentX,currentY){
var position = camera.getPosition();
var target = camera.getTarget();
//左右平移
if(currentX != previousX){
var bLeft = currentX < previousX ? true:false;
var plumbVector = getPlumbVector(camera.getLightDirection(), bLeft);
position.x += plumbVector.x;
position.z += plumbVector.z;
target.x += plumbVector.x;
target.z += plumbVector.z;
}
//前後平移
if(currentY != previousY){
var bForward = currentY < previousY ? true:false;
var forwardVector = getForwardVector(camera.getLightDirection(), bForward);
position.x += forwardVector.x;
position.y += forwardVector.y;
position.z += forwardVector.z;
target.x += forwardVector.x;
target.y += forwardVector.y;
target.z += forwardVector.z;
}
camera.look(position,target);
}
function onMouseWheel(evt){
var scale = 0.0;
if (evt.wheelDelta ){
if(evt.wheelDelta > 0){
scale = 0.9;
}
else if(evt.wheelDelta < 0){
scale = 1.1;
}
}
else if(evt.detail){
if(evt.detail < 0){
scale = 0.9;
}
else if(evt.detail > 0){
scale = 1.1;
}
}
var distance = camera.getViewFrustumDistance();
camera.setViewFrustumDistance(distance*scale,true);
}
function onMouseUp(evt){
bMouseDown = false;
dojo.disconnect(handleMouseMove);
previousX = -1;
previousY = -1;
}
function getPlumbVector(direction,bLeft){
direction.y = 0;
direction.normalize();
var plumbVector = new World.Vector(-direction.z,0,direction.x);
plumbVector.normalize();
return plumbVector;
}
function getForwardVector(direction,bForward){
direction.y = 0;
direction.normalize();
if (bForward){
direction.x *= -1;
direction.z *= -1;
}
return direction;
}
下面是前端程式碼:
<!DOCTYPE HTML>
<html>
<head>
<title>WebGL Terrain Map</title>
<meta http-equiv="Content-Type" content="text/html"/>
<meta name="charset" content="utf-8"/>
<!--<link rel="stylesheet" type="text/css" href="http://serverapi.arcgisonline.com/jsapi/arcgis/3.1/js/dojo/dijit/themes/claro/claro.css">-->
<link rel="stylesheet" type="text/css" href="http://localhost/arcgis_js_api/library/3.1/jsapi/js/dojo/dijit/themes/claro/claro.css">
<style type="text/css">
html, body { margin: 0; padding: 0; width: 100%; height: 100% ;font-family:"Times New Roman",Georgia,Serif;}
div { margin: 0; padding: 0 }
.head{width:100%; height:25px; background-color:#5998DD;color:#ffffff;font-size:13px;
line-height:25px;border-top-left-radius:5px;border-top-right-radius:5px;-webkit-border-top-left-radius:5px;
-webkit-border-top-right-radius:5px;-moz-border-radius-topleft:5px;-moz-border-radius-topright:5px;}
.tip{font-size:8px;display:block;height:8px;margin-left:3px;margin-top:9px;}
</style>
<!--<script src='http://serverapi.arcgisonline.com/jsapi/arcgis/?v=3.1' data-dojo-config='parseOnLoad: true'></script>-->
<script src='http://localhost/arcgis_js_api/library/3.1/jsapi/' data-dojo-config='parseOnLoad: true'></script>
<script type="text/javascript" src="World0.3.5.js"></script>
<script type="text/javascript" src="CanvasEventHandler.js"></script>
<script type="text/javascript">
dojo.require("esri.map");
dojo.require("esri.tasks.geometry");
dojo.require("dojo.parser");
dojo.require("dijit.form.HorizontalSlider");
dojo.require("dijit.layout.BorderContainer");
dojo.require("dijit.layout.ContentPane");
var elevationData = [],imageName = "";
var vertexShaderContent,fragmentShaderContent,camera,scene,renderer,heightMap;
var map,dynamicLayer,bMap2D = true,previousExtent = null;;
var mapServiceUrl = "http://services.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer";
var streetMapUrl = "http://services.arcgisonline.com/ArcGIS/rest/services/World_Street_Map/MapServer";
var topoMapUrl = "http://services.arcgisonline.com/ArcGIS/rest/services/World_Topo_Map/MapServer";
var squareCenterExtent;
var geometryServiceUrl = "http://tasks.arcgisonline.com/ArcGIS/rest/services/Geometry/GeometryServer";
var getElevationDataUrl = "http://sampleserver4.arcgisonline.com/ArcGIS/rest/services/Elevation/ESRI_Elevation_World/MapServer/exts/ElevationsSOE/ElevationLayers/1/GetElevationData";
function getShaderContent(shaderType){
var shaderUrl;
if(shaderType == "VERTEX_SHADER"){
shaderUrl = "VertexShader.txt";
}
else if(shaderType == "FRAGMENT_SHADER"){
shaderUrl = "FragmentShader.txt";
}
var xhrArgs = {
url:shaderUrl,
handleAs:"text",
sync:true,
preventCache:true,
load:function(data){
if(shaderType == "VERTEX_SHADER"){
vertexShaderContent = data;
}
else if(shaderType == "FRAGMENT_SHADER"){
fragmentShaderContent = data;
}
},
error:function(error){
alert("獲取ShaderContent出錯!");
}
};
dojo.xhrGet(xhrArgs);
}
function getDefaultElevationData(dataName){
var xhrArgs = {
url:"ElevationData/"+dataName,
handleAs:"text",
sync:true,
preventCache:true,
load:function(data){
elevationData = [];
var strElevationArray = data.split(',');
for(var i=0;i<strElevationArray.length;i++){
elevationData.push(parseFloat(strElevationArray[i]));
}
},
error:function(error){
alert("載入預設高程資料出錯!")
}
};
dojo.xhrGet(xhrArgs);
}
function startWebGL(){
getShaderContent("VERTEX_SHADER");
getShaderContent("FRAGMENT_SHADER");
//getDefaultElevationData("50X50.txt");
renderer = new World.WebGLRenderer(dojo.byId("canvasId"),vertexShaderContent,fragmentShaderContent);
//World.enableAmbientLight();
//World.enableParallelLlight(new World.Vector(0,-30,-50),new World.Vertice(0,0,0.8));
World.disableAmbientLight();
World.disableParallelLlight();
camera = new World.PerspectiveCamera(45,1,1.0,100.0);
camera.look(new World.Vertice(0,30,50),new World.Vertice(0,-30,-50));
scene = new World.Scene();
//heightMap = new World.HeightMap(rowCount,columnCount,elevationData,"MapImages/"+"terrain512.jpg");
//scene.add(heightMap);
renderer.bindScene(scene);
renderer.bindCamera(camera);
renderer.setIfAutoRefresh(false);
}
function judgeExtentEqual(ext1,ext2){
if(ext1 && ext2){
var sr1 = ext1.spatialReference;
var sr2 = ext2.spatialReference;
function judgeNumberEqual(a,b){
var c = Math.abs(a-b);
if(c <= 100)
return true;
}
if(judgeNumberEqual(ext1.xmin,ext2.xmin) && judgeNumberEqual(ext1.ymin,ext2.ymin) && judgeNumberEqual(ext1.xmax,ext2.xmax) && judgeNumberEqual(ext1.ymax,ext2.ymax) && sr1.wkid == sr2.wkid){
return true;
}
}
return false;
}
function getSquareCenterExtent(){
var offset = 0;
if (map.extent.getHeight() < map.extent.getWidth()) {
offset = map.extent.getHeight() / 2;
}
else {
offset = map.extent.getWidth() / 2;
}
var p = map.extent.getCenter();
var squareExtent = esri.geometry.Extent(p.x - offset, p.y - offset, p.x + offset, p.y + offset, map.extent.spatialReference)
return squareExtent;
}
function showTerrain3D(bUpdate,row,column,elevations,mapImageName){
if(bUpdate == true){
scene.remove(heightMap);
var scale = parseFloat(dojo.byId("labelStretch").innerHTML);
heightMap = new World.HeightMap(row, column, elevations, "MapImages/"+mapImageName,scale);
scene.add(heightMap);
}
renderer.setIfAutoRefresh(true);
dojo.byId("mapId").style.display = "none";
dojo.byId("canvasId").style.display = "block";
dojo.byId("iSpring").style.visibility = "visible";
dojo.byId("btnSwitch").innerHTML = "轉換為2D檢視";
dojo.byId("btnSwitch").disabled = false;
dijit.byId("sliderStretch")._setDisabledAttr(false);
previousExtent = map.extent;
bMap2D = false;
}
function startSwitch(bSwitchTo3D){
squareCenterExtent = getSquareCenterExtent();
if(bSwitchTo3D == true){
setIfDisableControls(true);
var currentExtent = map.extent;
//第一個判斷的順序要放在首位
if(judgeExtentEqual(currentExtent,previousExtent)){
showTerrain3D(false);
}
else{
var imageSize = dojo.byId("selectImageSize").value;
getMapImageUrl(dynamicLayer,squareCenterExtent,imageSize,imageSize);
}
}
else{
renderer.setIfAutoRefresh(false);
setIfDisableControls(false);
dojo.byId("canvasId").style.display = "none";
dojo.byId("iSpring").style.visibility = "hidden";
dojo.byId("mapId").style.display = "block";
dojo.byId("btnSwitch").innerHTML = "轉換為3D檢視";
dijit.byId("sliderStretch")._setDisabledAttr(true);
bMap2D = true;
}
}
function setIfDisableControls(bDisable){
dojo.byId("btnSwitch").disabled = bDisable;
dojo.byId("btnFullExtent").disabled = bDisable;
dojo.byId("radioSatelite").disabled = bDisable;
dojo.byId("radioStreet").disabled = bDisable;
dojo.byId("radioTopo").disabled = bDisable;
dijit.byId("sliderGridSize")._setDisabledAttr(bDisable);
dojo.byId("selectImageSize").disabled = bDisable;
}
function getMapImageUrl(dynamicLayer,extent,imageWidth,imageHeight){
var imageParameters = new esri.layers.ImageParameters();
imageParameters.bbox = extent;
imageParameters.format = "jpeg";
imageParameters.width = imageWidth;
imageParameters.height = imageHeight;
imageParameters.imageSpatialReference = map.spatialReference;
dynamicLayer.exportMapImage(imageParameters,function(mapImage){
tryStoreMapImage(mapImage.href);
});
}
function tryStoreMapImage(imageUrl){
var xhrArgs = {
url:"proxy.ashx?requestType=getImage&imageUrl="+imageUrl,
handleAs:"text",
sync:false,
preventCache:true,
load:function(data){
imageName = data;
//alert("儲存影象完成!");
var gridSize = Math.round(dijit.byId("sliderGridSize").value);
getCurrentElevationData(gridSize,gridSize);
},
error:function(error){
alert("儲存影象出錯!");
startSwitch(false);
}
};
dojo.xhrGet(xhrArgs);
}
function getCurrentElevationData(rows,columns){
var extent = squareCenterExtent;
var xhrArgs = {
url:"proxy.ashx?requestType=getElevation",
content:{
Rows:rows,
Columns:columns,
xmin:extent.xmin,
ymin:extent.ymin,
xmax:extent.xmax,
ymax:extent.ymax,
wkid:extent.spatialReference.wkid
},
handleAs:"text",
sync:false,
preventCache:true,
load:function(data){
//alert("獲取高程資料完成!");
succeedGetElevationData(data);
},
error:function(error){
alert("獲取高程出錯!");
startSwitch(false);
}
};
dojo.xhrGet(xhrArgs);
}
function succeedGetElevationData(data){
var info = data.split(";");
var row = parseFloat(info[0]);//一定要將string轉換成float
var column = parseFloat(info[1]);//一定要將string轉換成float
var strElevations = info[2];
elevationData = [];
var strElevationArray = strElevations.split(',');
for(var i=0;i<strElevationArray.length;i++){
elevationData.push(parseFloat(strElevationArray[i]));
}
showTerrain3D(true,row,column,elevationData,imageName);
}
function initLayout(){
var sliderGridSize = new dijit.form.HorizontalSlider({
name: "sliderGridSize",
value: 50,
minimum: 1,
maximum: 100,
intermediateChanges: true,
style: "width:175px;float:left;",
onChange:function(value){
dojo.byId("labelGridSize").innerHTML = Math.round(value);
}
}, "sliderGridSize");
var sliderStretch = new dijit.form.HorizontalSlider({
name:"sliderStretch",
value:1,
minimum:0,
maximum:4,
intermediateChanges:true,
style:"width:175px;float:left;",
onChange:function(value){
var scale = Math.round(value*10)/10;//var scale = Math.round(value);
dojo.byId("labelStretch").innerHTML = scale;
heightMap.heightScale = scale;
}
},"sliderStretch");
sliderStretch._setDisabledAttr(true);//開始的時候要禁用拉伸滑塊
var clientWidth = document.body.clientWidth < 1024 ? 1024:document.body.clientWidth;
var clientHeight = document.body.clientHeight < 600 ? 600:document.body.clientHeight;
var height = clientHeight - 10 - 10;//
var viewerWidth = clientWidth - 220 - 10;//
document.getElementById("controls").style.height = height+"px";
document.getElementById("controlsContent").style.height = (height - 25) + "px";
document.getElementById("viewer").style.height = height+"px";
document.getElementById("mapId").style.height = (height-25)+"px";
document.getElementById("canvasId").height = height-25;
document.getElementById("viewer").style.width = viewerWidth + "px";
document.getElementById("mapId").style.width = viewerWidth + "px";
document.getElementById("canvasId").width = viewerWidth;
}
function initMap(){
map = new esri.Map("mapId");
var basemap = new esri.layers.ArcGISTiledMapServiceLayer(mapServiceUrl);
map.addLayer(basemap);
dynamicLayer = new esri.layers.ArcGISDynamicMapServiceLayer(mapServiceUrl,{imageFormat:"jpg"});
dojo.connect(dynamicLayer,'onError',function(){
alert("載入動態圖層出錯!");
});
dojo.connect(map, 'onLoad',function(theMap){
dojo.connect(dojo.byId('mapId'),'onresize',map, map.resize);
dojo.connect(theMap, "onMouseDown",function(evt){
console.log(evt.mapPoint);
});
});
}
function initEvents(){
dojo.connect(dojo.byId("canvasId"),"mousedown",onMouseDown);
dojo.connect(dojo.byId("canvasId"),"onmouseup",onMouseUp);
dojo.connect(dojo.byId("canvasId"),'mousewheel',onMouseWheel);
dojo.connect(dojo.byId("canvasId"),'DOMMouseScroll',onMouseWheel);
}
function initAll(){
initLayout();
initEvents();
initMap();
startWebGL();
}
function showTest3D(){
getDefaultElevationData("Test.txt");
showTerrain3D(true,50,50,elevationData,"Test.jpg");
}
dojo.addOnLoad(initAll);
</script>
</head>
<body class="claro" style="background-color:#D8DCE0;" onselectstart="return false;">
<div id="main">
<div id="controls" style="width:200px;height:650px;position:absolute;left:10px;top:10px;">
<div class="head" style="text-align:left;"> Controls</div>
<div id="controlsContent" style="height:624px;position:relative;background-color:#ffffff;">
<div style="width:100%;height:40px;padding-top:5px;background-color:LightBlue;">
<button id="btnSwitch" style="display:block;height:30px;margin-left:auto;margin-right:auto;"
onclick="javascript:bMap2D==true?startSwitch(true):startSwitch(false);">轉換為3D檢視</button>
</div>
<span class="tip">導航</span>
<image src="Images/horizontal.png" />
<button id="btnFullExtent" style="display:block;height:30px;" onclick="map.setExtent(getFullExtent());">全圖</button>
<span class="tip">地圖</span>
<image src="Images/horizontal.png" />
<input type="radio" id="radioSatelite" name="radioMap"/>Satelite<br/>
<input type="radio" id="radioStreet" name="radioMap"/>Street<br/>
<input type="radio" id="radioTopo" name="radioMap"/>Topo<br/>
<span class="tip">高程格網大小</span>
<image src="Images/horizontal.png" />
<div id="sliderGridSize"></div><label id="labelGridSize" style="float:left;">50</label></br>
<span class="tip">影象大小</span>
<image src="Images/horizontal.png" />
<select id="selectImageSize" style="display:block;width:190px;margin:0 auto;">
<option value="64">64</option>
<option value="128">128</option>
<option value="256">256</option>
<option value="512" >512</option>
<option value="1024" selected>1024</option>
<option value="2048">2048</option>
</select>
<a href="http://cn.khronos.org/" target="_blank" style="display:block;position:absolute;bottom:0px;">
<div style="width:116px;height:77px;background-image:url(Images/WebGL.png);"></div>
</a>
<span class="tip">拉伸係數</span>
<image src="Images/horizontal.png" />
<div id="sliderStretch"></div><label id="labelStretch" style="float:left;">1</label>
</div>
</div>
<div id="viewer" style="width:1040px;height:650px;position:absolute;left:220px;top:10px;">
<div class="head">
Viewer
<a href="http://weibo.com/iispring" target="_blank" style="float:right;text-decoration:none;color:#ffffff;margin-right:7px;text-align:center;line-height:25px;">About iSpring</a>
</div>
<div id="mapId" style="width:1040px;height:625px;background-color:#666666;"></div>
<canvas id="canvasId" width="1040" height="625" style="display:none;"></canvas>
<a id="iSpring" style="visibility:hidden;diaplay:block;position:absolute;bottom:0px;right:0px;" href="http://weibo.com/iispring" target="_blank">
<div style="width:81px;height:50px;background-image:url(Images/iSpring.png);"></div>
</a>
</div>
</div>
</body>
</html>
下面是所使用的代理proxy.ashx
<%@ WebHandler Language="C#" Class="proxy" %>
using System;
using System.Web;
using System.IO;
using System.Drawing;
using System.Text;
public class proxy : IHttpHandler
{
public void ProcessRequest(HttpContext context)
{
string requestType = context.Request["requestType"];
if (requestType == "getImage")
{
string url = context.Request["imageUrl"];
System.Net.WebRequest request = System.Net.WebRequest.Create(new Uri(url));
request.Method = context.Request.HttpMethod;
request.ContentType = "application/x-www-form-urlencoded";
System.Net.WebResponse response = request.GetResponse();
Stream stream = response.GetResponseStream();
Image img = Image.FromStream(stream);
int index = url.LastIndexOf('/');
string imageName = url.Remove(0, index + 1);
string baseDirectory = System.AppDomain.CurrentDomain.BaseDirectory;
string physicPath = baseDirectory + "\\MapImages\\" + imageName;
img.Save(physicPath);
context.Response.Write(imageName);
context.Response.End();
}
else if (requestType == "getElevation")
{
string Rows = context.Request["Rows"];
string Columns = context.Request["Columns"];
string xmin = context.Request["xmin"].Trim(); ;
string ymin = context.Request["ymin"].Trim(); ;
string xmax = context.Request["xmax"].Trim();
string ymax = context.Request["ymax"].Trim();
string wkid = context.Request["wkid"];
string baseUrl = "http://sampleserver4.arcgisonline.com/ArcGIS/rest/services/Elevation/ESRI_Elevation_World/MapServer/exts/ElevationsSOE/ElevationLayers/1/GetElevationData";
string paras = "?Extent=%7B%22xmin%22%3A" + xmin + "%2C%0D%0A%22ymin%22%3A" + ymin + "%2C%0D%0A%22xmax%22%3A" + xmax + "%2C%0D%0A%22ymax%22%3A" + ymax + "%2C%0D%0A%22spatialReference%22%3A%7B%22wkid%22%3A" + wkid + "%7D%7D&Rows=" + Rows + "&Columns=" + Columns + "&f=pjson";
string url = baseUrl + paras;
System.Net.WebRequest request = System.Net.WebRequest.Create(new Uri(url));
request.Method = context.Request.HttpMethod;
request.ContentType = "application/x-www-form-urlencoded";
System.Net.WebResponse response = request.GetResponse();
Stream stream = response.GetResponseStream();
StreamReader sr = new StreamReader(stream);
string strResponse = sr.ReadToEnd();
int index1 = strResponse.LastIndexOf('[') + 1;
string str = strResponse.Remove(0, index1);
int index2 = str.LastIndexOf(']');
string result = str.Remove(index2);
result = result.Replace("\r\n", "").Replace(" ", "");//類似於這樣32767,32767,-3175,384,1983,-208
//假設高程6500對應著地球投影面長寬的1/10
float width = int.Parse(Rows);
string[] results = result.Split(',');
StringBuilder LastOutput = new StringBuilder();
LastOutput.Append(Rows + ";" + Columns + ";");
for (int i = 0; i < results.Length; i++)
{
string strElevation = results[i];
int Elevation = int.Parse(strElevation);
if (Elevation == 32767)
{
Elevation = 0;
}
float handledElevation = width / 10 * Elevation / 6500;
string strHandledElevation = handledElevation.ToString();
if (i != results.Length - 1)
{
LastOutput.Append(strHandledElevation + ",");
}
else
{
LastOutput.Append(strHandledElevation);
}
}
context.Response.ContentType = "text/plain";
context.Response.Write(LastOutput.ToString());
context.Response.End();
}
}
public bool IsReusable
{
get {
return false;
}
}
}
轉載請註明出處
相關文章
- WebGL自學課程(9):WebGL框架World.js(0.3.5版本)Web框架JS
- WebGL自學課程(14):WebGL使用Mipmap紋理Web
- WebGL自學課程(16):WebGlobe實現的基本演算法原理Web演算法
- WebGL自學課程(15):WebGL在WebGIS上的應用——WebGlobeWeb
- WebGL自學課程(12):光照模型與渲染方式Web模型
- WebGL自學課程(13):獲得三維拾取向量Web
- WebGL自學課程(10):通過OpenStreetMap獲取資料繪製地球Web
- WebGL自學課程(11):ELSL著色器程式設計中內建的運算子與函式Web程式設計函式
- nodejs + cheerio + Promise(bluebird庫實現)抓取慕課網nodejs課程資料NodeJSPromise
- WebGL自學課程(7):WebGL載入跨域紋理出錯Cross-origin image load denied by Cross-Origin Resource Sharing policy.Web跨域ROS
- nodejs實現restful APINodeJSRESTAPI
- Python課程程式碼實現Python
- webgl實現故障效果Web
- webgl實現火焰效果Web
- js拖拽原理及簡單實現(渣渣自學)JS
- 課程 1: JSON 解析JSON
- WebGL 實現雨水特效實驗Web特效
- WebGL沒有通道APIWebAPI
- 課程實踐(二)
- 課程實踐(一)
- vuejs 免費視訊課程VueJS
- 理解JSON:3分鐘課程JSON
- 如何實現現代應用的落地-CSDN公開課-專題視訊課程
- 課程實踐(二)續
- 課程實踐(一)續
- 使用jsdoc-toolkit實現JS API文件自動化JSAPI
- OCP課程8:SQL之使用子查詢SQL
- k8s :kube-apiserver RESTful API 實現 - StorageK8SAPIServerREST
- 密碼學課程設計 - 混合密碼的實現密碼學
- 專案實戰視訊課程:美團網(Vue2+Node.js+Express+支付+Electron)-李寧-專題視訊課程...VueNode.jsExpress
- 計算機實驗室之樹莓派:課程 8 螢幕03計算機樹莓派
- 實踐JavaWeb課程專案JavaWeb
- 課程實踐(一)續1
- 基於HTML5 WebGL實現 json工控風機葉輪旋轉HTMLWebJSON
- 我的慕課實戰課程上線了
- 某課網「vue.js 入門基礎」課程札記Vue.js
- 自學Node.JsNode.js
- js 技術自學JS