马黑黑 发表于 2025-5-11 07:52

古域传说

<style>
        #tz { --state: running; margin: 30px 0; left: calc(50% - 81px); transform: translateX(-50%); width: clamp(600px, 90vw, 1400px); min-height: 80vh; aspect-ratio: 16/9; background: url('https://638183.freep.cn/638183/t24/webp3/gyiu.webp') no-repeat center/cover; box-shadow: 2px 2px 8px #000; position: relative; }
        #player { position: absolute; left: 30px; top: 30px; z-index: 10; clip-path: circle(45%); transition: filter .7s; cursor: pointer; animation: rot 2s infinite linear var(--state); }
        #player:hover { filter: invert(0.8); }
        #btnFs { left: 20px; bottom: 20px; color: #eee; text-align: center; }
        #btnFs:hover { color: red; }
        #vid {position: absolute; width: 100%; height: 100%; opacity: .8; object-fit: cover; mask: radial-gradient(transparent 20%, red); -webkit-mask: radial-gradient(transparent 20%, red); pointer-events: none; }
        @keyframes rot { to { transform: rotate(360deg); } }
</style>

<div id="tz">
        <audio id="aud" src="https://music.163.com/song/media/outer/url?id=473968018" autoplay loop></audio>
        <video id="vid" src="https://bpic.588ku.com/video_listen/588ku_video/22/11/04/09/01/30/video6364646a16ae6.mp4" autoplay loop muted></video>
        <img id="player" src="https://638183.freep.cn/638183/small/tdji.png" width="10%" title="播放/暂停" />
</div>

<script type="module">

import * as THREE from 'https://esm.sh/three';
import { OrbitControls } from "https://esm.sh/three/examples/jsm/controls/OrbitControls";
import { FS } from 'https://638183.freep.cn/638183/web/ku/fscreen.js';

let rotRaf, isPaused = true, balls = [];

const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(60, tz.offsetWidth / tz.offsetHeight, 0.1, 1000);
camera.position.z = 20;
const renderer = new THREE.WebGLRenderer({ alpha: true });
renderer.setSize(tz.offsetWidth, tz.offsetHeight);
tz.appendChild(renderer.domElement);

const geometry = new THREE.SphereGeometry(2, 32, 32, 0, 0.75 * Math.PI, 0, 1.5 * Math.PI);
for (let i = 0; i <= 8; i++) {
        const material = new THREE.MeshNormalMaterial({
                transparent: true,
                opacity: 0.65,
                wireframe: true,
                side: THREE.DoubleSide
        });
        const ball = new THREE.Mesh(geometry, material);
        const radian = (360 / 8) * (Math.PI / 180);
        ball.rotateY(Math.random() * 180);
        if (i < 8) ball.position.set(8 * Math.sin(radian * i), 8 * Math.cos(radian * i), 0);
        balls.push(ball);
        scene.add(ball);
}

const controller = new OrbitControls(camera, renderer.domElement);
controller.autoRotate = true;

isPaused = aud.paused;

const mState = () => {
        isPaused = aud.paused;
        aud.paused ? cancelAnimationFrame(rotRaf) : rotating();
};

const rotating = () => {
        if (isPaused ) return;
        balls.forEach(b => b.rotation.y -= Math.random() * 0.05);
        controller.update();
        renderer.render(scene, camera);
        rotRaf = requestAnimationFrame(rotating);
};

window.onresize = () => renderer.setSize(tz.offsetWidth, tz.offsetHeight);

aud.onplaying = aud.onpause = () => mState();
aud.onseeked = () => cancelAnimationFrame(rotRaf);

rotating();
FS(tz, player);

</script>

马黑黑 发表于 2025-5-11 07:54

<div id="hEdiv" data-name="帖子代码"><pre id="hEpre">
&lt;style&gt;
        #tz { --state: running; margin: 30px 0; left: calc(50% - 81px); transform: translateX(-50%); width: clamp(600px, 90vw, 1400px); min-height: 80vh; aspect-ratio: 16/9; background: url('https://638183.freep.cn/638183/t24/webp3/gyiu.webp') no-repeat center/cover; box-shadow: 2px 2px 8px #000; position: relative; }
        #player { position: absolute; left: 30px; top: 30px; z-index: 10; clip-path: circle(45%); transition: filter .7s; cursor: pointer; animation: rot 2s infinite linear var(--state); }
        #player:hover { filter: invert(0.8); }
        #btnFs { left: 20px; bottom: 20px; color: #eee; text-align: center; }
        #btnFs:hover { color: red; }
        #vid {position: absolute; width: 100%; height: 100%; opacity: .8; object-fit: cover; mask: radial-gradient(transparent 20%, red); -webkit-mask: radial-gradient(transparent 20%, red); pointer-events: none; }
        @keyframes rot { to { transform: rotate(360deg); } }
&lt;/style&gt;

&lt;div id="tz"&gt;
        &lt;audio id="aud" src="https://music.163.com/song/media/outer/url?id=473968018" autoplay loop&gt;&lt;/audio&gt;
        &lt;video id="vid" src="https://bpic.588ku.com/video_listen/588ku_video/22/11/04/09/01/30/video6364646a16ae6.mp4" autoplay loop muted&gt;&lt;/video&gt;
        &lt;img id="player" src="https://638183.freep.cn/638183/small/tdji.png" width="10%" title="播放/暂停" /&gt;
&lt;/div&gt;

&lt;script type="module"&gt;

import * as THREE from 'https://esm.sh/three';
import { OrbitControls } from "https://esm.sh/three/examples/jsm/controls/OrbitControls";
import { FS } from 'https://638183.freep.cn/638183/web/ku/fscreen.js';

let rotRaf, isPaused = true, balls = [];

const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(60, tz.offsetWidth / tz.offsetHeight, 0.1, 1000);
camera.position.z = 20;
const renderer = new THREE.WebGLRenderer({ alpha: true });
renderer.setSize(tz.offsetWidth, tz.offsetHeight);
tz.appendChild(renderer.domElement);

const geometry = new THREE.SphereGeometry(2, 32, 32, 0, 0.75 * Math.PI, 0, 1.5 * Math.PI);
for (let i = 0; i &lt;= 8; i++) {
        const material = new THREE.MeshNormalMaterial({
                transparent: true,
                opacity: 0.65,
                wireframe: true,
                side: THREE.DoubleSide
        });
        const ball = new THREE.Mesh(geometry, material);
        const radian = (360 / 8) * (Math.PI / 180);
        ball.rotateY(Math.random() * 180);
        if (i &lt; 8) ball.position.set(8 * Math.sin(radian * i), 8 * Math.cos(radian * i), 0);
        balls.push(ball);
        scene.add(ball);
}

const controller = new OrbitControls(camera, renderer.domElement);
controller.autoRotate = true;

isPaused = aud.paused;

const mState = () =&gt; {
        isPaused = aud.paused;
        aud.paused ? cancelAnimationFrame(rotRaf) : rotating();
};

const rotating = () =&gt; {
        if (isPaused ) return;
        balls.forEach(b =&gt; b.rotation.y -= Math.random() * 0.05);
        controller.update();
        renderer.render(scene, camera);
        rotRaf = requestAnimationFrame(rotating);
};

window.onresize = () =&gt; renderer.setSize(tz.offsetWidth, tz.offsetHeight);

aud.onplaying = aud.onpause = () =&gt; mState();
aud.onseeked = () =&gt; cancelAnimationFrame(rotRaf);

rotating();
FS(tz, player);

&lt;/script&gt;
</pre></div>

<script type="module">
import hlight from 'https://638183.freep.cn/638183/web/helight/helight1.js';
hlight.hl(hEdiv, hEpre);
</script>

马黑黑 发表于 2025-5-11 08:34

本帖最后由 马黑黑 于 2025-5-11 08:38 编辑

特效部分用 three.js 实现,具体做法:在场景(scene)中创建球形几何体(SphereGeomotry),使用法向量材质(MessNormalMaterial)在几何体上构建球体。球体共9个,前8个绕圈圈排列,老9放在默认位置成为核心;动画方面,通过自定义 rotating 函数实现,一是每一个球体以不同的速度旋转,二是整体旋转——实际上是相机通过轨道控制 OrbitControls 控件绕圈圈旋转得到的效果。

关于球体的形状:

创建 SphereGeometry 球形几何体时使用了完整参数——

const geometry = new THREE.SphereGeometry(
    2, // 参数一 :几何体半径,默认值 1
    32, // 参数二:水平方向的分段数,决定了球体的水平切片数,默认 8
    32, // 参数三:垂直方向的分段数,决定了球体的纵向切片数,默认 8
    0, // 参数四:纵向起始角度,默认 0
   0.75 * Math.PI, // 参数五:纵向角度范围,默认 Math.PI * 2
    0, // 参数六:横向起始角度,默认 0
    1.5 * Math.PI // 参数七:横向角度范围,默认 Math.PI * 2
);

配置参数决定球形几何体的最终形状,但和构建材质息息相关。帖子所使用的法向量材质 MeshNormalMaterial,同样做了相关配置——


const material = new THREE.MeshNormalMaterial({
    transparent: true, // 开启透明度
    opacity: 0.65, // 透明度为 0.35
    wireframe: true, // 使用线框
    side: THREE.DoubleSide // 双面渲染
});



法向量材质可配置的对象参数细节也很多,有材质共性的属性,也有法向量材质特定的属性,这里全部使用共性的对象参数。


注意,同样是参数配置,几何体是直接参数,材质是对象参数(所以有花括号 {} 和花括号里的各键值对)。


几何体是骨架,材质是血肉,二者通过 THREE.Mesh 构建出最终球体(代码在40行)。

关于自适应窗口变化:

代码在 60 行,如下——

    window.onresize = () => renderer.setSize(tz.offsetWidth, tz.offsetHeight);

它的作用是窗口改变尺寸时,渲染器重设渲染范围,场景会跟随帖子容器的尺寸改变而改变。全屏与常规模式切换等操作时会触发此事件,从而达到整体效果自适应window视口的目的。

主要是这些,不尽事宜后续交流补充。

马黑黑 发表于 2025-5-11 08:41

由于使用了 OrbitControl 控件,球体接受鼠标相关操作,翻转、伸缩等都可以

花飞飞 发表于 2025-5-11 09:01

昨天的小球自身是不动的,今天是有自转的。
材质自带的色彩吧,与这五彩缤纷的夜光搭起来跟霓虹灯似的,好看。
刚才不明白为何球体不是整圆,原来这么长的一串参数就是设置用的。
2, 32, 32, 0, 0.75 * Math.PI, 0, 1.5
不过我把代码复制到编辑器中它不显示效果{:4_173:}。
不知是哪里的原因,我再试试去

花飞飞 发表于 2025-5-11 09:05

几何体是直接参数,材质是对象参数,
直接参数比较好理解,对象参数得再琢磨琢磨{:4_173:}
3D效果更需要窗口自适应,手机看直接从横版变成竖版,
球体和小播位置按比例更改,没有任何变形,效果完美得震撼。。。

花飞飞 发表于 2025-5-11 09:07

马黑黑 发表于 2025-5-11 08:41
由于使用了 OrbitControl 控件,球体接受鼠标相关操作,翻转、伸缩等都可以

手机拖动球体,随意更改成水平或者任意位置,或者缩放改变大小,体验感舒适。。。{:4_173:}
这个3D球体效果太完美了。。

花飞飞 发表于 2025-5-11 09:09

这球体材质这么处理一下,加上这种色彩渲染,跟透明泡泡一样,特别漂亮
音乐很熟悉。。好听。{:4_173:}

马黑黑 发表于 2025-5-11 09:10

关于 mState 音频联动控制函数,有一个情况说明一下:

所导入的模块 https://638183.freep.cn/638183/web/ku/fscreen.js 已经存在一个同名函数,该模块同时也在监听 audio 音频控件的 playing、pause 等事件。那么,为什么不会引发冲突呢?

这就是模块的高明之处:它具备隔离机制,业已存在的变量、对象、函数等,除非下 FS 这是个公开的,其余不明文 export 出来的,都处在隔离状态,该干嘛干嘛,不管别人干的是啥。

帖子需要通过音频的 paused 属性确定 isPaused 布尔变量的值从而达到联动控制three.js动画的目的,所以也要对audio控件进行相关监听。两边的同时监听一般情况下无法实现,有了ES6模块的工作机制,这些都能实现了。

马黑黑 发表于 2025-5-11 09:13

花飞飞 发表于 2025-5-11 09:01
昨天的小球自身是不动的,今天是有自转的。
材质自带的色彩吧,与这五彩缤纷的夜光搭起来跟霓虹灯似的,好 ...

我就是在简易编辑器那里制作的帖子,同时也用 pencil code 制作,以比较一些设置的区别。代码就从它们那里复制过来。

马黑黑 发表于 2025-5-11 09:13

花飞飞 发表于 2025-5-11 09:09
这球体材质这么处理一下,加上这种色彩渲染,跟透明泡泡一样,特别漂亮
音乐很熟悉。。好听。

国风音乐

马黑黑 发表于 2025-5-11 09:13

花飞飞 发表于 2025-5-11 09:07
手机拖动球体,随意更改成水平或者任意位置,或者缩放改变大小,体验感舒适。。。
这个3D球体 ...

应该可以的

马黑黑 发表于 2025-5-11 09:14

花飞飞 发表于 2025-5-11 09:05
几何体是直接参数,材质是对象参数,
直接参数比较好理解,对象参数得再琢磨琢磨
3D效果更需要 ...

{:4_190:}

花飞飞 发表于 2025-5-11 09:14

马黑黑 发表于 2025-5-11 09:13
我就是在简易编辑器那里制作的帖子,同时也用 pencil code 制作,以比较一些设置的区别。代码就从它们那 ...

奇了,我复制你的代码过去的呀,两个都试了。。
我再试一下

花飞飞 发表于 2025-5-11 09:16

马黑黑 发表于 2025-5-11 09:13
国风音乐

我可以确定我听过{:4_170:}音盲能记得个熟悉的还算是好了

马黑黑 发表于 2025-5-11 09:16

花飞飞 发表于 2025-5-11 09:14
奇了,我复制你的代码过去的呀,两个都试了。。
我再试一下

我也复制了一次过去,没问题,都行

马黑黑 发表于 2025-5-11 09:16

花飞飞 发表于 2025-5-11 09:16
我可以确定我听过音盲能记得个熟悉的还算是好了

天才中的天才

花飞飞 发表于 2025-5-11 09:17

马黑黑 发表于 2025-5-11 09:13
应该可以的

手机一样灵活,完全没有滞涩之感,特别好玩

花飞飞 发表于 2025-5-11 09:18

马黑黑 发表于 2025-5-11 09:14


{:4_173:}这个咖啡得送给你,这么复杂的效果,辛苦了

花飞飞 发表于 2025-5-11 09:19

马黑黑 发表于 2025-5-11 09:16
我也复制了一次过去,没问题,都行

算了,我老是整出各种问题,还不造是哪里的问题{:4_173:}
页: [1] 2 3 4
查看完整版本: 古域传说