效果图

一个<canvas>标签是核心,就像一块黑色的数字画布,所有的 3D 时钟元素都将在这里绘制。它没有预设的图案,却能承载 WebGL 渲染的复杂图形,是连接代码与视觉的 “桥梁”;
两个隐藏的<script>标签分别存放 “顶点着色器” 和 “片段着色器” 代码 —— 这两个特殊脚本是 WebGL 的 “画笔”,负责计算图形的形状和颜色,而 HTML 的作用就是安全地 “保管” 它们,等待 JavaScript 调用。
没有多余的元素,HTML 只用最基础的结构定义了 “哪里可以绘图” 和 “绘图规则存放在哪里”,就像搭建了一个空旷的剧场,只等后续的 “灯光” 和 “演员” 登场。
它将页面背景设为深灰色(background: #333),像一块低调的幕布,让时钟的光影效果更突出 —— 就像电影院的黑暗环境能让银幕更明亮;
让画布(<canvas>)占满整个屏幕(width: 100%; height: 100%),确保无论在手机还是电脑上,时钟都能完整展示,没有边框限制,增强沉浸感;
隐藏页面滚动条(overflow: hidden),避免多余的元素干扰视线,让注意力完全集中在旋转的时钟上。
这些看似简单的样式,实则是在精心控制 “视觉焦点”:用深色背景衬托时钟的亮色,用全屏布局强化 3D 效果的纵深感,让技术生成的图形更具观赏性。
这个 3D 时钟最神奇的地方 —— 会随时间转动、会随鼠标旋转、会按时段变色 —— 全靠 JavaScript 这个 “导演” 在幕后指挥。它的工作可以分为三个核心部分:
1. 让时钟 “走” 起来:时间的精准计算
JavaScript 会实时获取当前时间(小时、分钟、秒),并将这些数字转化为 3D 时钟的 “运动指令”:
秒针每 1 秒转动一个刻度,分针每 60 秒转动一格,时针每小时缓慢移动 —— 就像机械钟的齿轮传动,只不过这里用代码计算角度;
为了让转动更自然,它还会添加 “动画过渡”:比如秒针不是突然跳格,而是有微小的加速和减速,模拟真实时钟的惯性。
2. 用 WebGL 绘制 3D 效果:着色器的 “魔法”
JavaScript 会调用 HTML 中存放的着色器代码,让它们在画布上绘制出立体的时钟:
顶点着色器负责计算每个 “刻度” 的 3D 位置,让时钟看起来有厚度和弧度,不是扁平的图案;
片段着色器则计算每个像素的颜色:白天时用暖黄色调,夜晚时切换为蓝紫色,凌晨和傍晚又有不同的过渡色,就像时钟在 “感知” 时间的流逝;
还会添加光影效果:模拟光线照射在时钟上的明暗变化,让立体效果更真实,比如刻度的边缘会有高光,背面则略显阴暗。
3. 响应互动:让时钟 “跟着鼠标转”
当你移动鼠标时,JavaScript 会捕捉鼠标位置,实时调整时钟的视角:
鼠标上下移动,时钟会前后倾斜;左右移动,时钟会左右旋转,就像你在亲手转动一个实物时钟;
为了让旋转更顺滑,它会给视角变化添加 “缓冲”,不会因为鼠标突然移动而显得生硬 —— 这种细节让互动感更自然。
此外,JavaScript 还负责 “舞台维护”:比如窗口大小变化时,自动调整画布尺寸;确保着色器代码正确运行,避免图形出错。就像一个全能导演,既管演员(时钟)的动作,又管舞台(画布)的适配。
<html lang="en"><head><meta charset="UTF-8" /><title>基于 WebGL 的着色器时钟</title><style>html {height: 100%;}body {background: #333;overflow: hidden;padding: 0;margin: 0;width: 100%;height: 100%;display: flex;align-items: center;}canvas {height: 100%;width: 100%;}</style></head><body><canvas id="canvas"></canvas><!-- VertexShader code here --><script id="vertexShader" type="x-shader/x-vertex">#version 300 esprecision highp float;in vec4 position;void main() {gl_Position = vec4( position );}</script><!-- FragmentShader code here --><script id="fragmentShader" type="x-shader/x-fragment">#version 300 es#if __VERSION__ < 130#define TEXTURE2D texture2D#else#define TEXTURE2D texture#endifprecision highp float;out vec4 fragColor;uniform vec2 u_resolution;uniform vec4 u_mouse;uniform float u_time;uniform float u_date;#define R u_resolution#define T u_time#define M u_mouse#define PI 3.14159265359#define PI2 6.28318530718#define MIN_DIST 1e-4#define MAX_DIST 50./**License: Creative Commons Attribution-NonCommercial-ShareAlike 3.0 Unported LicenseDial Clock ConceptWas kicking this around in my brain to make a clock shader.The math is a little messy and there's some trick logicaround the hours to make the dials work.*/// numbers from SDF Shapesfloat box( in vec2 p, in vec2 b ){vec2 d = abs(p)-b;return length(max(d,0.))+min(max(d.x,d.y),0.);}float rbox( in vec2 p, in vec2 b, in vec4 r ) {r.xy = (p.x>0.)?r.xy : r.zw;r.x = (p.y>0.)?r.x : r.y;vec2 q = abs(p)-b+r.x;return min(max(q.x,q.y),0.)+length(max(q,0.))-r.x;}const vec4 bz = vec4(.075,.055,.035,.0);const vec4 bo = vec4(.175,.15,.075,.025);const vec2 oa = vec2(.15,.1);// numberfloat get0(vec2 p) {float bt = max(rbox(p,vec2(.15,.175),bz.xxxx),-rbox(p,vec2(.1,.125),bz.zzzz));return bt;}float get1(vec2 p) {float bt = box(p,vec2(.025,.175));return bt;}float get2(vec2 p) {float bt = max(rbox(p-vec2(.0,.075),oa,bz.xxww),-rbox(p-vec2(-.075,.075),vec2(.175,.05),bz.zzww));bt = max(bt,-box(p-vec2(-.085,.045),vec2(.08,.08)));float bb = max(rbox(p+vec2(.0,.075),oa,bz.wwxx),-rbox(p+vec2(-.075,.075),vec2(.175,.05),bz.wwzz));bb = max(bb,-box(p+vec2(-.085,.045),vec2(.08,.08)));return min(bt,bb);}float get3(vec2 p) {vec2 of = vec2(0,.075);float bt = max(rbox(p-of,oa,bz.xxww),-rbox(p-of+vec2(.05,0),vec2(.15,.05),bz.zzww));float bb = max(rbox(p+of,oa,bz.xxww),-rbox(p+of+vec2(.05,0),vec2(.15,.05),bz.zzww));return min(bt,bb);}float get4(vec2 p) {float bt = min(box(vec2(p.x-.125,p.y),vec2(.025,.175)),rbox(p-vec2(.0,.075),oa,bz.wwwx));bt = max(bt,-rbox(p-vec2(0,.115),vec2(.1,.095),bz.wwwz));return bt;}float get5(vec2 p) {float bt = max(rbox(p-vec2(.0,.075),oa,bz.wwwx),-rbox(p-vec2(.075,.075),vec2(.175,.05),bz.wwwz));bt = max(bt,-box(p-vec2(.085,.045),vec2(.08,.08)));float bb = max(rbox(p+vec2(.0,.075),oa,bz.xxww),-rbox(p+vec2(.075,.075),vec2(.175,.05),bz.zzww));bb = max(bb,-box(p+vec2(.085,.045),vec2(.08,.08)));return min(bt,bb);}float get6(vec2 p) {float bt = max(rbox(p-vec2(.0,.075),oa,bz.wwxw),-rbox(p-vec2(.075,.075),vec2(.175,.05),bz.wwzw));bt = max(bt,-box(p-vec2(.085,.045),vec2(.08,.08)));float bb = max(rbox(p+vec2(.0,.075),oa,bz.xxwx),-rbox(p+vec2(.0,.075),vec2(.1,.05),bz.zzwz));return min(bt,bb);}float get7(vec2 p) {float bt = max(rbox(p+vec2(0,.0),vec2(.15,.175),bz.xwww),-rbox(p+vec2(.05,.045),vec2(.15,.175),bz.zwww));return bt;}float get8(vec2 p) {float bt = max(rbox(p-vec2(.0,.075),oa,bz.xxxx),-rbox(p-vec2(.0,.075),vec2(.1,.05),bz.zzzz));float bb = max(rbox(p+vec2(.0,.075),oa,bz.xxxx),-rbox(p+vec2(.0,.075),vec2(.1,.05),bz.zzzz));return min(bt,bb);}float get9(vec2 p) {float bt = max(rbox(p-vec2(.0,.075),oa,bz.xwxx),-rbox(p-vec2(.0,.075),vec2(.1,.05),bz.zwzz));float bb = max(rbox(p+vec2(.0,.075),oa,bz.wxxw),-rbox(p+vec2(.075,-.02),vec2(.175,.15),bz.zzwz));return min(bt,bb);}// the shader code belowfloat hash21(vec2 p) {return fract(sin(dot(p, vec2(27.69, 32.53)))*437.53);}mat2 rot(float a){ return mat2(cos(a),sin(a),-sin(a),cos(a)); }// timing functionsfloat lerp (float b, float e, float t) { return clamp((t - b) / (e - b), 0., 1.); }float eic (float t) { return t*t*t; }float opx(in float d, in float z, in float h){vec2 w = vec2( d, abs(z) - h );return min(max(w.x, w.y), 0.) + length(max(w, 0.));}// globalsfloat h1,h2,m1,m2,s1,s2,tmod;vec3 hp,hit;vec2 map(vec3 p) {vec2 res = vec2(1e5,0);vec3 q = p + vec3(2,0,0);float ofs = 2.8;float mn = -.62831853071;float sz = 1.3;vec2 q1 = q.yz;vec2 q2 = q.yz;q1*=rot(ofs+(h1*mn));q2*=rot(ofs+(h2*mn));float d = opx(abs(length(q1)-sz)-.01,q.x,.25)-.025;if(d<res.x) {res = vec2(d,1);hit = vec3(q.x,q1.y,-q1.x);}float e = opx(abs(length(q2)-sz)-.01,q.x-.75,.25)-.025;if(e<res.x) {res = vec2(e,2);hit = vec3(q.x,q2.y,-q2.x);}q = p + vec3(0,0,0);q1 = p.yz;q2 = p.yz;q1*=rot(ofs+(m1*mn));q2*=rot(ofs+(m2*mn));float g = opx(abs(length(q1)-sz)-.01,q.x,.25)-.025;if(g<res.x) {res = vec2(g,3);hit = vec3(q.x,q1.y,-q1.x);}float h = opx(abs(length(q2)-sz)-.01,q.x-.75,.25)-.025;if(h<res.x) {res = vec2(h,4);hit = vec3(q.x,q2.y,-q2.x);}q = p - vec3(2,0,0);q1 = p.yz;q2 = p.yz;q1*=rot(ofs+(s1*mn));q2*=rot(ofs+(s2*mn));float j = opx(abs(length(q1)-sz)-.01,q.x,.25)-.025;if(j<res.x) {res = vec2(j,5);hit = vec3(q.x,q1.y,-q1.x);}float k = opx(abs(length(q2)-sz)-.01,q.x-.75,.25)-.025;if(k<res.x) {res = vec2(k,6);hit = vec3(q.x,q2.y,-q2.x);}float fl = length(p.zy)-.75;if(fl<res.x) {res = vec2(fl,10);hit = p;}return res;}vec3 normal(vec3 p, float t) {t*=MIN_DIST;float d = map(p).x;vec2 e = vec2(t,0);vec3 n = d - vec3(map(p-e.xyy).x,map(p-e.yxy).x,map(p-e.yyx).x);return normalize(n);}vec2 marcher(vec3 ro, vec3 rd, inout vec3 p, int steps) {float d=0.,m=0.;for(int i=0;i<steps;i++){vec2 t = map(p);d += t.x;m = t.y;p = ro + rd * d;if(abs(t.x)<d*MIN_DIST||d>75.) break;}return vec2(d,m);}float getDigits(vec2 nv,int dec) {float d = 1e5;if(dec == 0) d = get0(nv);if(dec == 1) d = get1(nv);if(dec == 2) d = get2(nv);if(dec == 3) d = get3(nv);if(dec == 4) d = get4(nv);if(dec == 5) d = get5(nv);if(dec == 6) d = get6(nv);if(dec == 7) d = get7(nv);if(dec == 8) d = get8(nv);if(dec == 9) d = get9(nv);return d;}void main( ){vec2 F = gl_FragCoord.xy;// time precalfloat idate = u_date;//debug var//T+3598.;//int sec = int(mod(idate,60.));int minute = int(mod(idate/60.,60.));int hour = int(mod(idate/3600.,12.));int ampm = int(mod(idate/3600.,24.));// global digitsfloat num = float(hour);if(num == 0.) num = 12.;h1 = floor(mod(num / pow(10.0,1.),10.0));h2 = floor(mod(num / pow(10.0,0.),10.0));num = float(minute);m1 = floor(mod(num / pow(10.0,1.),10.0));m2 = floor(mod(num / pow(10.0,0.),10.0));num = float(sec);s1 = floor(mod(num / pow(10.0,1.),10.0));s2 = floor(mod(num / pow(10.0,0.),10.0));// second dialsfloat t2 = lerp(0.,1.,mod(idate,1.));s2 = s2+eic(t2);float t1 = lerp(9.,10.,mod(idate,10.));s1 = s1+eic(t1);// minute dialsfloat t4 = lerp(59.,60.,mod(idate,60.));m2 = m2+eic(t4);float t3 = lerp(599.,600.,mod(idate,600.));m1 = m1+eic(t3);// hour dialsfloat t6 = lerp(3599.,3600.,mod(idate,3600.));h2 = h2+eic(t6);float t5 = lerp(3599.,3600.,mod(idate,3600.));if(hour == 0) hour = 12;if(hour == 9||hour>11) {h1 = h1+eic(t5);}//// uv ro + rdvec2 uv = (2.* F.xy-R.xy)/max(R.x,R.y);vec3 ro = vec3(.4,0,R.y<400.?6.:4.75);vec3 rd = normalize(vec3(uv, -1.0));// mouse //float x = M.xy==vec2(0) ? 0. : (M.y/R.y*.2-.1)*PI;float y = M.xy==vec2(0) ? 0. : (M.x/R.x*.2-.1)*PI;mat2 rx = rot(x+.2*cos(T*.325)), ry = rot(y+.15*sin(T*.25));ro.zy*=rx, ro.xz*=ry;rd.zy*=rx, rd.xz*=ry;vec3 C = vec3(0), p = ro;vec2 ray = marcher(ro,rd,p,100);float d = ray.x, m = ray.y;hp = hit;if(d<MAX_DIST) {vec3 n = normal(p,d);vec3 lpos = vec3(5,11,12);vec3 l = normalize(lpos-p);float diff = clamp(dot(n,l),.1,1.);float shdw = 1.;for( float t=.01;t<12.; ) {float h = map(p + l*t).x;if( h<MIN_DIST ) { shdw = 0.; break; }shdw = min(shdw, 32.*h/t);t += h * .95;if( shdw<MIN_DIST || t>42. ) break;}diff = mix(diff,diff*shdw,.75);float spec = .75 * pow(max(dot(normalize(p-ro),reflect(normalize(lpos),n)),0.),24.);// base color change depending on time of dayvec3 h = ampm<12?vec3(1,.635,0):ampm>19?vec3(.337,.082,.718):vec3(.8);if(m>0.) {vec2 uv = vec2(atan(hp.z,hp.y)/PI2,p.x);vec2 id = vec2(floor(uv.x*10.),1.);id.x = mod(abs(id.x+5.),10.);uv.x = fract(uv.x*10.)-.5;if(m==1.) {uv.y+=2.;id.x=mod(abs(id.x+6.),2.);}if(m==2.) uv.y+=1.25;if(m==3.) id.x=mod(abs(id.x+6.),6.);if(m==4.) uv.y-=.75;if(m==5.){uv.y-=2.;id.x=mod(abs(id.x+5.)+1.,6.);}if(m==6.) uv.y-=2.75;float px = 2./R.x;vec2 nv = vec2(uv.y,-uv.x)*1.25;float d = getDigits(nv,int(id.x));float c = length(uv)-.235;c=max(c,-d);// dial color change depending on time of dayvec3 clr = ampm<12?vec3(1.,.35,0):ampm>19?vec3(.68,.14,.98):vec3(.1,.9,.3);vec3 clx = ampm<12?vec3(1.):ampm>19?vec3(0.6):vec3(.3);vec3 cld = ampm<12?vec3(.7,.3,.0):ampm>19?vec3(0.1):vec3(.6);h = mix(h,clx,smoothstep(px,-px,c));h = mix(h,clr,smoothstep(px,-px,d));c = length(vec2(abs(uv.x)-.5,uv.y))-.175;h = mix(h,cld,smoothstep(px,-px,c));}if(m==10.) {// pattern color change depending on time of dayvec3 clr = ampm<12?vec3(.973,.89,.05):ampm>19?vec3(.235,.165,.75):vec3(.72,.93,.52);vec3 cld = ampm<12?vec3(.7,.3,.0):ampm>19?vec3(.001):vec3(.6);vec2 uv = vec2(atan(hp.z,hp.y)/PI2,p.x);uv.xy += T*vec2(-.02,1);vec2 sc = vec2(14.,3), id = floor(uv*sc);uv = fract(uv*sc)-.5;float px = 2./R.x, rnd = hash21(id);if(rnd>.5) uv.x*=-1.;float chk = mod(id.y + id.x,2.) * 2. - 1.;vec2 gx = length(uv-.5)<length(uv+.5)? vec2(uv-.5) : vec2(uv+.5);float tr = length(gx)-.5;if(ampm>19){tr= smoothstep(-px,px,abs(abs(tr)-.1)-.05);}else{tr= (chk>.5 ^^ rnd<.5) ? smoothstep(-px,px,tr) : smoothstep(px,-px,tr);}h = mix(clr, cld,tr);}C = h * diff;}// fog overlay - color change depending on time of dayvec3 clr = ampm<12?vec3(0.306,0.118,0.016):ampm>19?vec3(0.039,0.020,0.078):vec3(.25);C = mix(C,clr, 1.-exp(-.00325*d*d*d));// line fadingif((int(F.x)%6 == int(F.y)%6) && R.x>800. ) C = clr;// static effectC = mix(C,clamp(C*.8,vec3(0),vec3(1)),hash21(floor(uv.xy*30.)+uv));C = pow(C, vec3(.4545));fragColor = vec4(C,1);}</script><script src="https://fecoder-pic-1302080640.cos.ap-nanjing.myqcloud.com/twgl.min.js"></script><script>function _defineProperty(obj, key, value) {if (key in obj) {Object.defineProperty(obj, key, {value: value,enumerable: true,configurable: true,writable: true,});} else {obj[key] = value;}return obj;} // Mouse Class for movments and attaching to dom //class Mouse {constructor(element) {_defineProperty(this,"reset",() => {this.x =~~(document.documentElement.clientWidth,window.innerWidth || 0) / 2;this.y =~~(document.documentElement.clientHeight,window.innerHeight || 0) / 2;});this.element = element || window;this.drag = false;this.x =~~(document.documentElement.clientWidth, window.innerWidth || 0) /2;this.y =~~(document.documentElement.clientHeight, window.innerHeight || 0) /2;this.getCoordinates = this.getCoordinates.bind(this);this.events = ["mouseenter", "mousemove"];this.events.forEach((eventName) => {this.element.addEventListener(eventName, this.getCoordinates);});this.element.addEventListener("mousedown", () => {this.drag = true;});this.element.addEventListener("mouseup", () => {this.drag = false;});window.addEventListener("resize", this.reset);}getCoordinates(event) {event.preventDefault();if (this.drag) {this.x = event.pageX;this.y = event.pageY;}}}// WEBGL BOOTSTRAP TWGL.jsconst glcanvas = document.getElementById("canvas");const gl = glcanvas.getContext("webgl2");// Fractal code in HTML window - Fragment Shader //const programInfo = twgl.createProgramInfo(gl, ["vertexShader","fragmentShader",]);const arrays = {position: [-1, -1, 0, 1, -1, 0, -1, 1, 0, -1, 1, 0, 1, -1, 0, 1, 1, 0],};const bufferInfo = twgl.createBufferInfoFromArrays(gl, arrays);const mouse = new Mouse(glcanvas);let umouse = [gl.canvas.width / 2, gl.canvas.height / 2, 0, 0];let tmouse = umouse;let uniforms;const ct = new Date();const timenow =ct.getHours() * 60.0 * 60 +ct.getMinutes() * 60 +ct.getSeconds() +ct.getMilliseconds() / 1000.0;// RENDER LOOPconst render = (time) => {twgl.resizeCanvasToDisplaySize(gl.canvas, 1.0);gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);const factor = 0.15;umouse = [mouse.x, mouse.y, 0];tmouse[0] = tmouse[0] - (tmouse[0] - umouse[0]) * factor;tmouse[1] = tmouse[1] - (tmouse[1] - umouse[1]) * factor;tmouse[2] = mouse.drag ? 1 : -1;const mytime = time / 1000;uniforms = {u_time: mytime,u_mouse: tmouse,u_date: mytime + timenow,u_resolution: [gl.canvas.width, gl.canvas.height],};gl.useProgram(programInfo.program);twgl.setBuffersAndAttributes(gl, programInfo, bufferInfo);twgl.setUniforms(programInfo, uniforms);twgl.drawBufferInfo(gl, bufferInfo);requestAnimationFrame(render);};// DOM READYwindow.addEventListener("DOMContentLoaded", (event) => {requestAnimationFrame(render);});</script></body></html>
此外,JavaScript 还负责 “舞台维护”:比如窗口大小变化时,自动调整画布尺寸;确保着色器代码正确运行,避免图形出错。就像一个全能导演,既管演员(时钟)的动作,又管舞台(画布)的适配。
从 HTML 搭建的 “舞台”,到 CSS 营造的 “氛围”,再到 JavaScript 驱动的 “动态与互动”,三个技术分工协作,把一串代码变成了一个会呼吸、会互动、有时间感的 3D 时钟。当你看着它随时间缓缓转动,随鼠标轻轻倾斜,甚至能通过颜色变化感知昼夜时,很难不感叹:技术与艺术的结合,原来可以这么迷人。
这或许就是 WebGL 的魅力 —— 用代码在二维的屏幕上,创造出仿佛能触摸的三维世界,而网页三巨头,就是实现这场魔术的关键道具。

