不止天气卡片,deepseek-v4-proflash cc实战测试
- 内容介绍
- 文章标签
- 相关推荐
前言
备受期待的DeepSeek V4终于发布了,相信很多人都想要看到这个模型在Coding任务中的实际表现。按传统来说,我们应该跑一个天气卡片,但是我一直觉得只有天气卡片完全反映不出来模型真正的能力,于是我决定来点创新的实战测试。
为保证本测试接近实际使用场景,测试全程使用Claude Code v2.1.86,官方正价API,模型映射配置如下所示:
图片862×234 16 KB
[!NOTE]
本次测试中DeepSeek V4所编写的源码可在 Raven95676/ds_v4_test: DeepSeek V4 Test - Codeberg.org 获取。真实环境测试仅保留Diff文件。由于时间以及精力还有财力限制,测试可能片面,欢迎各位补充测试用例。
综上,让我们开始测试吧!
希望测完了我不会变成负翁
独立项目从零生成测试
很多情况下,我们有了灵感都会让AI来开荒。我们首先来测试AI从零生成的能力。
天气卡片
首先是我们最为经典的天气卡片。
Prompt:
请编写一个单文件的HTML,实现一个现代化的天气卡片应用。所有的CSS和JS必须内联在这个HTML文件中,不允许引入外部的UI组件库。界面设计要求具备毛玻璃效果,包含天气图标、温度、地点、湿度、风速等元素,排版要求高水准的美感。
成果展示:
图片709×672 52.6 KB
尽管没有要求,DeepSeek V4依然接入了实际可用的数据源,填入OpenWeatherMap API Key后:
图片647×623 41.2 KB
算法可视化
[!NOTE]
本项测试使用npm create vite@latest的TypeScript+React+React Compiler模板创建了一个空项目,DeepSeek V4将在这个空项目的基础上开发。本项测试无额外预装依赖。
Prompt:
使用React+TypeScript编写一个算法可视化网站,包含排序算法可视化和寻路算法可视化两个模块,通过顶部导航标签页切换。使用纯React状态与CSS实现,不依赖任何第三方动画库或状态管理库。具体要求如下:
## 排序算法可视化(标签页1)
1. 随机生成包含20个整数的数组,用垂直柱状图展示。
2. 提供下拉菜单选择算法:冒泡排序、选择排序、插入排序、快速排序。
3. 排序过程逐步动画展示。
## 寻路算法可视化(标签页2)
1. 显示一个20×20的网格。允许用户交互编辑网格。
2. 提供下拉菜单选择寻路算法:Dijkstra、A*、BFS、DFS。
3. 寻路过程逐步动画展示。
成果展示:
图片1300×629 15.5 KB
图片741×831 14.8 KB
3D三角形旋转
[!NOTE]
本项测试使用npm create vite@latest的TypeScript+React+React Compiler模板创建了一个空项目,DeepSeek V4将在这个空项目的基础上开发。本项测试额外预装依赖three @react-three/fiber @react-three/drei @types/three。
Prompt:
使用React+TypeScript+React Three Fiber+Three.js编写一个3D场景。场景要求:
1. 场景中央显示一个彩色的3D等边三角形(红、绿、蓝),背景颜色为深灰色,带有简单的网格地面辅助线。
2. 三角形绕自身Y轴以恒定速度旋转。
3. 允许用户通过鼠标拖拽旋转整个场景视角,支持滚轮缩放。
成果展示:
图片598×645 28.3 KB
爬虫编写
[!NOTE]
本项测试使用uv init创建了一个空项目,预装依赖playwright,启用chrome-devtoolsMCP,选取盗版小说网站https://m.bqgl.cc/look/7546/作为评估示例。测试行为严格限于非商业性的技术验证,且遵循低频请求原则。如相关权利人认为该示例侵犯了网站的合法权益,请与我联系。
Prompt:
此项目为Python爬虫,目标网址为https://m.snapd.net/read/165986/。请使用chrome-devtools
MCP分析目标网站并完成项目,依赖管理使用uv,使用playwright进行抓取。有以下要求:
1. 解析出小说的所有章节列表,提取章节正文内容,清理多余的HTML标签,清理非小说正文的广告内容,保留纯文本段落。
2. 将所有章节按顺序合并保存到一个txt文件中,每章标题作为一行,空一行,接着是正文内容,然后两个空行分隔下一章。
3. 请求之间设置1秒的延迟,实现基本的错误处理和重试机制。
成果展示:
图片926×909 111 KB
戴森云模拟
[!NOTE]
本项测试使用npm create vite@latest的TypeScript+React+React Compiler模板创建了一个空项目,DeepSeek V4将在这个空项目的基础上开发。本项测试额外预装依赖three @react-three/fiber @react-three/drei @types/three,启用chrome-devtoolsMCP。
Prompt:
使用React+TypeScript+React Three Fiber+Three.js编写一个戴森云模拟场景。场景要求:
1. 场景中心有一颗发光恒星,发出暖黄色光芒,并带有光晕效果。
2. 恒星周围分布着大量小型蓝白色发光粒子,它们大致位于一个球壳范围内,形成不规则的云状结构。所有粒子以不同的角速度绕恒星中心公转,且有小幅度的随机径向偏移运动。
3. 允许用户通过鼠标拖拽旋转整个场景视角,支持滚轮缩放,右上角显示FPS等数据。
4. 添加基本的星场背景。
5. 小型蓝白色发光粒子需要允许用户通过滑块调整数量,数量范围在50-10000,请在不减少粒子数量的情况下保证性能。
请使用chrome-devtools MCP对成果进行验证。
成果展示:
screenshotfinal929×865 95.7 KB
真实环境测试
当然,我们不仅需要AI能够从零开荒,修复已有问题的能力也是很重要的。接下来我们测试DeepSeek V4在真实环境中的表现。
这个测试是选择一个已被修复的issue,然后分析AI是如何对这个问题进行修复的。
[!NOTE]
在实际应用中,不建议向开源项目贡献纯AI生成代码,尤其是在开源项目明确拒绝的情况下。
cpython
Crash on _ssl__SSLContext_load_cert_chain_impl (requests running w/ cert in multi-threading)
已打开 09:16AM - 26 May 25 UTC 已关闭 01:27PM - 08 Oct 25 UTC Conobi extension-modules type-crash topic-SSL# Crash report ### What happened? Hi. We've been investigating random crashes …of our FastAPI application for over 6 months, and we think we've found the culprit. When **calling requests with a custom cert (like in the code below) in a multi-threaded paradigm**, it can crashes. On some versions, like 3.12/3.13, it can in some case even block Python in a zombie state, where the process isn't killed but keep being hung. I tested on all the versions mentionned, and **the crash happened on all versions**. In the latest ones (>=3.12), it feels like I get more often double free. ```python import threading from concurrent.futures import ThreadPoolExecutor from datetime import datetime, timedelta from pathlib import Path import requests from cryptography import x509 from cryptography.hazmat.primitives import hashes, serialization from cryptography.hazmat.primitives.asymmetric import rsa from cryptography.hazmat.primitives.serialization import pkcs12 from cryptography.x509.oid import NameOID CERT_PEM = "client_cert.pem" KEY_PEM = "client_key.pem" PFX_FILE = "client_cert.pfx" CERT_PASSWORD = b"password" # For PFX export def generate_and_save_cert() -> None: """Generate RSA key and self-signed cert, save PEM and PFX. Can be commented out. """ key = rsa.generate_private_key( public_exponent=65537, key_size=2048, ) subject = issuer = x509.Name( [ x509.NameAttribute(NameOID.COUNTRY_NAME, "US"), x509.NameAttribute(NameOID.STATE_OR_PROVINCE_NAME, "California"), x509.NameAttribute(NameOID.LOCALITY_NAME, "San Francisco"), x509.NameAttribute(NameOID.ORGANIZATION_NAME, "Test Org"), x509.NameAttribute(NameOID.COMMON_NAME, "localhost"), ] ) cert = ( x509.CertificateBuilder() .subject_name(subject) .issuer_name(issuer) .public_key(key.public_key()) .serial_number(x509.random_serial_number()) .not_valid_before( datetime.now( datetime.utcnow() # for backward compatibility ) ) .not_valid_after( datetime.now( datetime.utcnow() # for backward compatibility ) + timedelta(days=365) ) .sign(key, hashes.SHA256()) ) Path(CERT_PEM).write_bytes(cert.public_bytes(serialization.Encoding.PEM)) Path(KEY_PEM).write_bytes( key.private_bytes( encoding=serialization.Encoding.PEM, format=serialization.PrivateFormat.TraditionalOpenSSL, encryption_algorithm=serialization.NoEncryption(), ) ) pfx = pkcs12.serialize_key_and_certificates( name=b"client", key=key, cert=cert, cas=None, encryption_algorithm=serialization.BestAvailableEncryption(CERT_PASSWORD), ) Path(PFX_FILE).write_bytes(pfx) def post_with_cert(url: str, idx: int) -> None: """Perform a POST request using PEM cert.""" try: response = requests.get( url, data={"test": f"thread-{idx}"}, cert=(CERT_PEM, KEY_PEM), # PEM files timeout=10, ) print(f"Thread {idx}: Status {response.status_code}") except Exception as exc: print(f"Thread {idx}: Exception {exc}") def main() -> None: generate_and_save_cert() url = "https://example.com" # <-- Change this! with ThreadPoolExecutor(max_workers=150) as executor: for i in range(150): executor.submit(post_with_cert, url, i) if __name__ == "__main__": main() ``` Here's the crash dump: ``` Core was generated by `/usr/local/bin/python3.15 foo.py'. Program terminated with signal SIGABRT, Aborted. #0 0x0000771454ecf624 in ?? () from /usr/lib/libc.so.6 [Current thread is 1 (Thread 0x7713720006c0 (LWP 87696))] #0 0x0000771454ecf624 in ?? () from /usr/lib/libc.so.6 #1 0x0000771454e75ba0 in raise () from /usr/lib/libc.so.6 #2 0x0000771454e5d582 in abort () from /usr/lib/libc.so.6 #3 0x0000771454e5e3bf in ?? () from /usr/lib/libc.so.6 #4 0x0000771454ed9765 in ?? () from /usr/lib/libc.so.6 #5 0x0000771454edbc8a in ?? () from /usr/lib/libc.so.6 #6 0x0000771454ede9ab in free () from /usr/lib/libc.so.6 #7 0x0000771453dc2eb5 in RSA_free () from /usr/lib/libcrypto.so.3 #8 0x0000771453d5ebd2 in ?? () from /usr/lib/libcrypto.so.3 #9 0x0000771453d5f498 in EVP_PKEY_free () from /usr/lib/libcrypto.so.3 #10 0x0000771454198e8a in ?? () from /usr/lib/libssl.so.3 #11 0x000077145419e4d6 in SSL_CTX_use_PrivateKey_file () from /usr/lib/libssl.so.3 #12 0x000077145429f899 in _ssl__SSLContext_load_cert_chain_impl (self=0x7714536f9b50, certfile=<optimized out>, keyfile=<optimized out>, password=<optimized out>) at ./Modules/_ssl.c:4148 #13 _ssl__SSLContext_load_cert_chain (self=0x7714536f9b50, args=<optimized out>, nargs=<optimized out>, kwnames=<optimized out>) at ./Modules/clinic/_ssl.c.h:1429 #14 0x0000586280553ca0 in _PyObject_VectorcallTstate (tstate=0x58629578de30, callable=0x771454583ba0, args=0x7714540e89e0, nargsf=<optimized out>, kwnames=0x0) at ./Include/internal/pycore_call.h:169 #15 PyObject_Vectorcall (callable=0x771454583ba0, args=args@entry=0x771371ffe468, nargsf=<optimized out>, kwnames=kwnames@entry=0x0) at Objects/call.c:327 #16 0x00005862806cda53 in _PyEval_EvalFrameDefault (tstate=0x58629578de30, frame=<optimized out>, throwflag=<optimized out>) at Python/generated_cases.c.h:1619 #17 0x00005862806d9efc in _PyEval_EvalFrame (tstate=0x58629578de30, frame=<optimized out>, throwflag=0) at ./Include/internal/pycore_ceval.h:119 #18 _PyEval_Vector (tstate=0x58629578de30, func=0x7714538ee090, locals=0x0, args=0x771452165530, argcount=<optimized out>, kwnames=<optimized out>) at Python/ceval.c:1961 #19 0x0000586280557fc2 in _PyObject_VectorcallTstate (tstate=0x58629578de30, callable=0x7714538ee090, args=0x771452165530, nargsf=4, kwnames=0x7714521800b0) at ./Include/internal/pycore_call.h:169 #20 method_vectorcall (method=<optimized out>, args=0x771452165538, nargsf=<optimized out>, kwnames=0x7714521800b0) at Objects/classobject.c:64 #21 0x0000586280555b98 in _PyVectorcall_Call (tstate=0x58629578de30, func=0x586280557e30 <method_vectorcall>, callable=0x77145217da00, tuple=<optimized out>, kwargs=<optimized out>) at Objects/call.c:285 #22 _PyObject_Call (tstate=0x58629578de30, callable=0x77145217da00, args=<optimized out>, kwargs=<optimized out>) at Objects/call.c:348 #23 PyObject_Call (callable=0x77145217da00, args=<optimized out>, kwargs=<optimized out>) at Objects/call.c:373 #24 0x00005862806cde12 in _PyEval_EvalFrameDefault (tstate=0x58629578de30, frame=<optimized out>, throwflag=<optimized out>) at Python/generated_cases.c.h:2654 #25 0x00005862806d9efc in _PyEval_EvalFrame (tstate=0x58629578de30, frame=<optimized out>, throwflag=0) at ./Include/internal/pycore_ceval.h:119 #26 _PyEval_Vector (tstate=0x58629578de30, func=0x77145361b1c0, locals=0x0, args=0x771452157730, argcount=<optimized out>, kwnames=<optimized out>) at Python/ceval.c:1961 #27 0x0000586280557fc2 in _PyObject_VectorcallTstate (tstate=0x58629578de30, callable=0x77145361b1c0, args=0x771452157730, nargsf=2, kwnames=0x771452164160) at ./Include/internal/pycore_call.h:169 #28 method_vectorcall (method=<optimized out>, args=0x771452157738, nargsf=<optimized out>, kwnames=0x771452164160) at Objects/classobject.c:64 #29 0x0000586280555b98 in _PyVectorcall_Call (tstate=0x58629578de30, func=0x586280557e30 <method_vectorcall>, callable=0x771452157680, tuple=<optimized out>, kwargs=<optimized out>) at Objects/call.c:285 #30 _PyObject_Call (tstate=0x58629578de30, callable=0x771452157680, args=<optimized out>, kwargs=<optimized out>) at Objects/call.c:348 #31 PyObject_Call (callable=0x771452157680, args=<optimized out>, kwargs=<optimized out>) at Objects/call.c:373 #32 0x00005862806cde12 in _PyEval_EvalFrameDefault (tstate=0x58629578de30, frame=<optimized out>, throwflag=<optimized out>) at Python/generated_cases.c.h:2654 #33 0x00005862806d9efc in _PyEval_EvalFrame (tstate=0x58629578de30, frame=<optimized out>, throwflag=0) at ./Include/internal/pycore_ceval.h:119 #34 _PyEval_Vector (tstate=0x58629578de30, func=0x77145361be20, locals=0x0, args=0x771452157b70, argcount=<optimized out>, kwnames=<optimized out>) at Python/ceval.c:1961 #35 0x0000586280557fc2 in _PyObject_VectorcallTstate (tstate=0x58629578de30, callable=0x77145361be20, args=0x771452157b70, nargsf=2, kwnames=0x771452164700) at ./Include/internal/pycore_call.h:169 #36 method_vectorcall (method=<optimized out>, args=0x771452157b78, nargsf=<optimized out>, kwnames=0x771452164700) at Objects/classobject.c:64 #37 0x0000586280555b98 in _PyVectorcall_Call (tstate=0x58629578de30, func=0x586280557e30 <method_vectorcall>, callable=0x771452156b80, tuple=<optimized out>, kwargs=<optimized out>) at Objects/call.c:285 #38 _PyObject_Call (tstate=0x58629578de30, callable=0x771452156b80, args=<optimized out>, kwargs=<optimized out>) at Objects/call.c:348 #39 PyObject_Call (callable=0x771452156b80, args=<optimized out>, kwargs=<optimized out>) at Objects/call.c:373 #40 0x00005862806cde12 in _PyEval_EvalFrameDefault (tstate=0x58629578de30, frame=<optimized out>, throwflag=<optimized out>) at Python/generated_cases.c.h:2654 #41 0x00005862806d9efc in _PyEval_EvalFrame (tstate=0x58629578de30, frame=<optimized out>, throwflag=0) at ./Include/internal/pycore_ceval.h:119 #42 _PyEval_Vector (tstate=0x58629578de30, func=0x77145361b8a0, locals=0x0, args=0x7714521553f0, argcount=<optimized out>, kwnames=<optimized out>) at Python/ceval.c:1961 #43 0x0000586280557fc2 in _PyObject_VectorcallTstate (tstate=0x58629578de30, callable=0x77145361b8a0, args=0x7714521553f0, nargsf=1, kwnames=0x771452127d60) at ./Include/internal/pycore_call.h:169 #44 method_vectorcall (method=<optimized out>, args=0x7714521553f8, nargsf=<optimized out>, kwnames=0x771452127d60) at Objects/classobject.c:64 #45 0x0000586280555b98 in _PyVectorcall_Call (tstate=0x58629578de30, func=0x586280557e30 <method_vectorcall>, callable=0x771452154c40, tuple=<optimized out>, kwargs=<optimized out>) at Objects/call.c:285 #46 _PyObject_Call (tstate=0x58629578de30, callable=0x771452154c40, args=<optimized out>, kwargs=<optimized out>) at Objects/call.c:348 #47 PyObject_Call (callable=0x771452154c40, args=<optimized out>, kwargs=<optimized out>) at Objects/call.c:373 #48 0x00005862806cde12 in _PyEval_EvalFrameDefault (tstate=0x58629578de30, frame=<optimized out>, throwflag=<optimized out>) at Python/generated_cases.c.h:2654 #49 0x00005862806d9efc in _PyEval_EvalFrame (tstate=0x58629578de30, frame=<optimized out>, throwflag=0) at ./Include/internal/pycore_ceval.h:119 #50 _PyEval_Vector (tstate=0x58629578de30, func=0x7714549d1170, locals=0x0, args=0x771371fff938, argcount=<optimized out>, kwnames=<optimized out>) at Python/ceval.c:1961 #51 0x000058628055802b in _PyObject_VectorcallTstate (tstate=0x58629578de30, callable=0x7714549d1170, args=0x771371fff938, nargsf=1, kwnames=0x0) at ./Include/internal/pycore_call.h:169 #52 method_vectorcall (method=<optimized out>, args=0x771371fffbd8, nargsf=<optimized out>, kwnames=0x0) at Objects/classobject.c:72 #53 0x00005862806f784c in _PyObject_VectorcallTstate (tstate=0x58629578de30, callable=0x771452154800, args=0x771371fffbd8, nargsf=<optimized out>, kwnames=0x0) at ./Include/internal/pycore_call.h:169 #54 context_run (self=0x7714521549c0, args=0x771371fffbd0, nargs=<optimized out>, kwnames=0x0) at Python/context.c:728 #55 0x00005862806ceefe in _PyEval_EvalFrameDefault (tstate=0x58629578de30, frame=<optimized out>, throwflag=<optimized out>) at Python/generated_cases.c.h:3764 #56 0x00005862806d9efc in _PyEval_EvalFrame (tstate=0x58629578de30, frame=<optimized out>, throwflag=0) at ./Include/internal/pycore_ceval.h:119 #57 _PyEval_Vector (tstate=0x58629578de30, func=0x7714549d1220, locals=0x0, args=0x771371fffdd8, argcount=<optimized out>, kwnames=<optimized out>) at Python/ceval.c:1961 #58 0x000058628055802b in _PyObject_VectorcallTstate (tstate=0x58629578de30, callable=0x7714549d1220, args=0x771371fffdd8, nargsf=1, kwnames=0x0) at ./Include/internal/pycore_call.h:169 #59 method_vectorcall (method=<optimized out>, args=0x586280a26958 <_PyRuntime+90104>, nargsf=<optimized out>, kwnames=0x0) at Objects/classobject.c:72 #60 0x00005862807ed50c in thread_run (boot_raw=0x58629578ddf0) at ./Modules/_threadmodule.c:368 #61 0x000058628076d957 in pythread_wrapper (arg=<optimized out>) at Python/thread_pthread.h:242 #62 0x0000771454ecd70a in ?? () from /usr/lib/libc.so.6 #63 0x0000771454f51aac in ?? () from /usr/lib/libc.so.6 ``` Since I did run the test on multiple versions: - I did run the test on official Docker Python images (except 3.9/3.10/3.14/3.15) - My Docker `openssl version`: `OpenSSL 3.0.16 11 Feb 2025 (Library: OpenSSL 3.0.16 11 Feb 2025)` - My host `openssl version`: `OpenSSL 3.4.1 11 Feb 2025 (Library: OpenSSL 3.4.1 11 Feb 2025)` - For CPython main branch, here's my `python -VV`: `Python 3.15.0a0 (heads/main-dirty:1729468016, May 23 2025, 14:52:55) [GCC 14.2.1 20250207]` - My distrib: `Manjaro Linux 25.0.0` - `/proc/version`: `Linux version 6.11.5-lqx1-1-lqx (linux-lqx@archlinux) (gcc (GCC) 14.2.1 20240910, GNU ld (GNU Binutils) 2.43.0) #1 ZEN SMP PREEMPT Tue, 22 Oct 2024 15:40:56 +0000` ### CPython versions tested on: 3.10, 3.11, 3.12, 3.13, 3.14, CPython main branch, 3.9 ### Operating systems tested on: Linux ### Output from running 'python -VV' on the command line: _No response_ ### Linked PRs * gh-134724 * gh-137107 * gh-137126
Prompt:Issue原文
成果分析:
总体来说就是头痛医头,脚痛医脚。DeepSeek V4确实可以找到问题并应用修复,但是它只会在一个点上进行修复,修复并不完整,缺乏全局视角。考虑到大多数LLM都有这个问题,并且为了公平起见毫无上下文引导,我个人认为倒是可以给出一个合格的成绩,不过距离真的生产可用还是有一段距离的。
网友解答:--【壹】--:
魔方花了我三块~!!!!
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>3x3 Rubik's Cube — Physics Grade</title>
<style>
:root {
--bg: #1a1a2e;
--surface: #16213e;
--accent: #e94560;
--text: #eaeaea;
--muted: rgba(255,255,255,0.45);
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
overflow: hidden;
background: var(--bg);
font-family: 'Inter', 'Segoe UI', system-ui, sans-serif;
user-select: none;
-webkit-user-select: none;
-webkit-tap-highlight-color: transparent;
height: 100dvh;
width: 100vw;
}
#canvas {
display: block;
position: fixed;
inset: 0;
}
#ui {
position: fixed;
bottom: 36px;
left: 50%;
transform: translateX(-50%);
display: flex;
gap: 14px;
z-index: 10;
}
#ui button {
padding: 13px 30px;
font-size: 15px;
font-weight: 600;
letter-spacing: 0.04em;
border: none;
border-radius: 10px;
cursor: pointer;
transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
outline: none;
position: relative;
overflow: hidden;
}
#scramble {
background: rgba(233, 69, 96, 0.85);
color: #fff;
box-shadow: 0 4px 24px rgba(233, 69, 96, 0.35);
}
#scramble:hover {
background: rgba(255, 90, 120, 0.95);
box-shadow: 0 6px 32px rgba(233, 69, 96, 0.5);
transform: translateY(-2px);
}
#reset {
background: rgba(22, 33, 62, 0.85);
color: #fff;
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.25);
}
#reset:hover {
background: rgba(30, 45, 80, 0.95);
box-shadow: 0 6px 32px rgba(0, 0, 0, 0.4);
transform: translateY(-2px);
}
button:active { transform: scale(0.96) !important; }
#info {
position: fixed;
bottom: 100px;
left: 50%;
transform: translateX(-50%);
color: var(--muted);
font-size: 13px;
z-index: 10;
pointer-events: none;
text-align: center;
letter-spacing: 0.03em;
transition: opacity 0.5s;
}
#status {
position: fixed;
top: 30px;
left: 50%;
transform: translateX(-50%);
color: var(--muted);
font-size: 12px;
z-index: 10;
pointer-events: none;
letter-spacing: 0.05em;
text-transform: uppercase;
opacity: 0;
transition: opacity 0.3s;
}
#status.visible { opacity: 1; }
</style>
</head>
<body>
<canvas id="canvas"></canvas>
<div id="status">Rotating...</div>
<div id="info">
<span style="opacity:0.8">Left-drag</span> rotate layer ·
<span style="opacity:0.8">Right-drag</span> orbit ·
<span style="opacity:0.8">Scroll</span> zoom
</div>
<div id="ui">
<button id="scramble">Scramble</button>
<button id="reset">Reset</button>
</div>
<!-- ==================== IMPORT MAP ==================== -->
<script type="importmap">
{
"imports": {
"three": "https://unpkg.com/three@0.160.0/build/three.module.js",
"three/addons/": "https://unpkg.com/three@0.160.0/examples/jsm/",
"@tweenjs/tween.js": "https://unpkg.com/@tweenjs/tween.js@18.6.4/dist/tween.esm.js"
}
}
</script>
<script type="module">
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
import * as TWEEN from '@tweenjs/tween.js';
// ====================================================================
// CONSTANTS & CONFIGURATION
// ====================================================================
const CUBIE_SIZE = 0.85; // edge length of each small cube
const SPACING = 0.15; // visible gap between cubies
const STEP = 1.0; // center-to-center distance (= CUBIE_SIZE + SPACING)
const EPSILON = 0.15; // threshold for layer membership check
const DRAG_SCALE = 1.0; // 1:1 tracking multiplier (computed dynamically)
// Standard Rubik's cube face colours (Western scheme)
const FACE_COLORS = {
right: '#C41E3A', // Red — +X
left: '#FF5800', // Orange — -X
up: '#FFFFFF', // White — +Y
down: '#FFD500', // Yellow — -Y
front: '#009E60', // Green — +Z
back: '#0051BA', // Blue — -Z
};
// Axes as 3D vectors for math operations
const AXIS_VEC = {
x: new THREE.Vector3(1, 0, 0),
y: new THREE.Vector3(0, 1, 0),
z: new THREE.Vector3(0, 0, 1),
};
// ====================================================================
// CANVAS TEXTURE GENERATOR (programmatic, zero external assets)
// ====================================================================
/**
* Draw a rounded-rectangle path on a 2D canvas context.
* Simulates the plastic sticker with chamfered corners.
*/
function roundedRect(ctx, x, y, w, h, r) {
ctx.beginPath();
ctx.moveTo(x + r, y);
ctx.lineTo(x + w - r, y);
ctx.arcTo(x + w, y, x + w, y + r, r);
ctx.lineTo(x + w, y + h - r);
ctx.arcTo(x + w, y + h, x + w - r, y + h, r);
ctx.lineTo(x + r, y + h);
ctx.arcTo(x, y + h, x, y + r, r);
ctx.lineTo(x, y + r);
ctx.arcTo(x, y, x + r, y, r);
ctx.closePath();
}
/**
* Create a CanvasTexture for one cubie face.
* @param {string|null} color Hex colour of the sticker, or null for black plastic.
* @param {number} size Texture resolution (square).
* @returns {THREE.CanvasTexture}
*/
function createFaceTexture(color, size = 256) {
const canvas = document.createElement('canvas');
canvas.width = size;
canvas.height = size;
const ctx = canvas.getContext('2d');
// --- Black plastic base (visible at edges / chamfer) ---
ctx.fillStyle = '#121212';
ctx.fillRect(0, 0, size, size);
if (color) {
const margin = size * 0.14; // black border width
const radius = size * 0.10; // corner radius
const x = margin;
const y = margin;
const w = size - 2 * margin;
const h = size - 2 * margin;
// --- Coloured sticker with rounded corners ---
ctx.fillStyle = color;
roundedRect(ctx, x, y, w, h, radius);
ctx.fill();
// --- Soft highlight gradient (top → bottom) for gloss ---
const grad = ctx.createLinearGradient(x, y, x, y + h);
grad.addColorStop(0, 'rgba(255,255,255,0.28)');
grad.addColorStop(0.35, 'rgba(255,255,255,0.04)');
grad.addColorStop(0.65, 'rgba(0,0,0,0.02)');
grad.addColorStop(1, 'rgba(0,0,0,0.18)');
ctx.fillStyle = grad;
roundedRect(ctx, x, y, w, h, radius);
ctx.fill();
// --- Subtle inner border (sticker edge) ---
ctx.strokeStyle = 'rgba(0,0,0,0.25)';
ctx.lineWidth = size * 0.012;
roundedRect(ctx, x, y, w, h, radius);
ctx.stroke();
}
const texture = new THREE.CanvasTexture(canvas);
texture.colorSpace = THREE.SRGBColorSpace;
texture.minFilter = THREE.LinearMipmapLinearFilter;
texture.magFilter = THREE.LinearFilter;
texture.generateMipmaps = true;
return texture;
}
// ====================================================================
// SCENE, RENDERER, CAMERA, LIGHTING
// ====================================================================
const canvas = document.getElementById('canvas');
const renderer = new THREE.WebGLRenderer({ canvas, antialias: true, alpha: false });
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure = 1.1;
renderer.outputColorSpace = THREE.SRGBColorSpace;
const scene = new THREE.Scene();
scene.background = new THREE.Color('#1a1a2e');
scene.fog = new THREE.Fog('#1a1a2e', 8, 30);
const camera = new THREE.PerspectiveCamera(
42, // FOV
window.innerWidth / window.innerHeight, // aspect
0.5, // near
40 // far
);
camera.position.set(5.5, 4.2, 6.8);
camera.lookAt(0, 0, 0);
// --- Ambient light (fills shadows, prevents pure black) ---
const ambient = new THREE.AmbientLight('#bcc8e0', 1.6);
scene.add(ambient);
// --- Hemisphere light (sky / ground colour gradient) ---
const hemi = new THREE.HemisphereLight('#8899cc', '#332244', 0.6);
scene.add(hemi);
// --- Key directional light (casts shadows) ---
const keyLight = new THREE.DirectionalLight('#ffffff', 4.5);
keyLight.position.set(7, 14, 5);
keyLight.castShadow = true;
keyLight.shadow.mapSize.width = 2048;
keyLight.shadow.mapSize.height = 2048;
keyLight.shadow.camera.near = 0.5;
keyLight.shadow.camera.far = 60;
keyLight.shadow.camera.left = -8;
keyLight.shadow.camera.right = 8;
keyLight.shadow.camera.top = 8;
keyLight.shadow.camera.bottom = -8;
keyLight.shadow.bias = -0.0004;
keyLight.shadow.normalBias = 0.02;
scene.add(keyLight);
// --- Fill light (opposite side, softer, no shadow) ---
const fillLight = new THREE.DirectionalLight('#8899cc', 1.2);
fillLight.position.set(-3, 2, -4);
scene.add(fillLight);
// ====================================================================
// GROUND PLANE (shadow receiver — enhances depth perception)
// ====================================================================
const groundGeo = new THREE.PlaneGeometry(20, 20);
const groundMat = new THREE.MeshStandardMaterial({
color: '#1a1a2e',
roughness: 0.9,
metalness: 0.0,
});
const ground = new THREE.Mesh(groundGeo, groundMat);
ground.rotation.x = -Math.PI / 2;
ground.position.y = -3.5;
ground.receiveShadow = true;
scene.add(ground);
// ====================================================================
// CUBE CONSTRUCTION — 27 independent cubies
// ====================================================================
/** @type {THREE.Mesh[]} */
const cubies = [];
// Pre-generate shared textures (7 total — 6 colours + 1 black)
const texBlack = createFaceTexture(null);
const texRight = createFaceTexture(FACE_COLORS.right);
const texLeft = createFaceTexture(FACE_COLORS.left);
const texUp = createFaceTexture(FACE_COLORS.up);
const texDown = createFaceTexture(FACE_COLORS.down);
const texFront = createFaceTexture(FACE_COLORS.front);
const texBack = createFaceTexture(FACE_COLORS.back);
// Shared materials (reused across cubies for efficiency)
function makeMat(tex, rough = 0.35, metal = 0.02) {
return new THREE.MeshStandardMaterial({
map: tex,
roughness: rough,
metalness: metal,
});
}
const matBlack = makeMat(texBlack, 0.55, 0.0);
const matRight = makeMat(texRight);
const matLeft = makeMat(texLeft);
const matUp = makeMat(texUp);
const matDown = makeMat(texDown);
const matFront = makeMat(texFront);
const matBack = makeMat(texBack);
const cubieGeo = new THREE.BoxGeometry(CUBIE_SIZE, CUBIE_SIZE, CUBIE_SIZE, 1, 1, 1);
// Build the 3×3×3 grid
for (let gx = -1; gx <= 1; gx++) {
for (let gy = -1; gy <= 1; gy++) {
for (let gz = -1; gz <= 1; gz++) {
// Only outer faces get a coloured sticker; inner faces are black.
// BoxGeometry material order: [+X, -X, +Y, -Y, +Z, -Z]
const materials = [
gx === 1 ? matRight : matBlack, // +X
gx === -1 ? matLeft : matBlack, // -X
gy === 1 ? matUp : matBlack, // +Y
gy === -1 ? matDown : matBlack, // -Y
gz === 1 ? matFront : matBlack, // +Z
gz === -1 ? matBack : matBlack, // -Z
];
const cubie = new THREE.Mesh(cubieGeo, materials);
cubie.position.set(gx * STEP, gy * STEP, gz * STEP);
cubie.castShadow = true;
cubie.receiveShadow = true;
// Store initial grid index for potential use
cubie.userData = { initGrid: { x: gx, y: gy, z: gz } };
scene.add(cubie);
cubies.push(cubie);
}
}
}
// ====================================================================
// ORBIT CONTROLS (right-click drag to orbit; scroll to zoom)
// ====================================================================
const controls = new OrbitControls(camera, renderer.domElement);
controls.target.set(0, 0, 0);
controls.enableDamping = true;
controls.dampingFactor = 0.12;
controls.minDistance = 4.0;
controls.maxDistance = 14.0;
controls.maxPolarAngle = Math.PI * 0.72; // prevent going under the ground
// Disable left-click so it never conflicts with our layer-rotation gesture.
// OrbitControls reads `mouseButtons.LEFT` and skips if the value is not a
// recognised MOUSE button constant (we use -1 to guarantee a no-match).
controls.mouseButtons = {
LEFT: -1, // disabled
MIDDLE: THREE.MOUSE.DOLLY,
RIGHT: THREE.MOUSE.ROTATE,
};
controls.update();
// Prevent the browser context menu on right-click over the canvas
renderer.domElement.addEventListener('contextmenu', e => e.preventDefault());
// ====================================================================
// INTERACTION STATE MACHINE
// ====================================================================
const raycaster = new THREE.Raycaster();
raycaster.far = 12;
/** One-shot state for an active finger-drag rotation */
let active = null; // { pivot, cubies, axis3D, axisStr, tangent2D, pxPerRad, startPointer }
/** True while a Tween / scramble animation is playing */
let isAnimating = false;
// Pointer tracking
const pointer = new THREE.Vector2();
const pointerStart = new THREE.Vector2();
// ====================================================================
// UTILITY: Project a 3D point to screen pixel coordinates
// ====================================================================
function toScreen(worldPos, cam, w, h) {
const ndc = worldPos.clone().project(cam);
return new THREE.Vector2(
(ndc.x * 0.5 + 0.5) * w,
(-ndc.y * 0.5 + 0.5) * h,
);
}
/**
* Project a 3D *direction* (not position) anchored at `origin` onto the
* screen and return a normalised 2D vector.
*/
function projectDirToScreen(origin3D, dir3D, cam, w, h) {
const a = toScreen(origin3D, cam, w, h);
const b = toScreen(origin3D.clone().add(dir3D), cam, w, h);
const v = new THREE.Vector2(b.x - a.x, b.y - a.y);
return v.length() < 1e-6 ? v : v.normalize();
}
/**
* Snap a 3D vector to the nearest cardinal axis (±X, ±Y, or ±Z).
* Used to clean up the raycast face normal.
*/
function snapToCardinal(v) {
const ax = Math.abs(v.x), ay = Math.abs(v.y), az = Math.abs(v.z);
const eps = 0.01;
if (ax >= ay && ax >= az) return new THREE.Vector3(v.x > eps ? 1 : (v.x < -eps ? -1 : 0), 0, 0);
if (ay >= ax && ay >= az) return new THREE.Vector3(0, v.y > eps ? 1 : (v.y < -eps ? -1 : 0), 0);
return new THREE.Vector3(0, 0, v.z > eps ? 1 : (v.z < -eps ? -1 : 0));
}
/**
* Given a face normal, return the two candidate rotation axes.
* Example: normal ≈ Z → candidates are X and Y.
*/
function getCandidateAxes(normal) {
const a = Math.abs(normal.x), b = Math.abs(normal.y), c = Math.abs(normal.z);
if (c >= a && c >= b) return ['x', 'y'];
if (b >= a && b >= c) return ['x', 'z'];
return ['y', 'z'];
}
// ====================================================================
// LAYER SELECTION (dynamic — based on world position, not hard-coded)
// ====================================================================
/**
* Return all cubies whose world position along `axis` equals `layerValue`
* (within EPSILON). This is the key to dynamic layer detection.
*/
function getLayerCubies(axis, layerValue) {
const key = ({ x: 'x', y: 'y', z: 'z' })[axis] || axis;
return cubies.filter(c => {
// Use getWorldPosition to handle any parent hierarchy correctly
const wp = new THREE.Vector3();
c.getWorldPosition(wp);
return Math.abs(wp[key] - layerValue) < EPSILON;
});
}
// ====================================================================
// PIVOT-BASED ROTATION ENGINE
// Core idea:
// 1. Create a temporary Object3D "pivot" at world origin.
// 2. pivot.attach(cubie) — magically preserves the cubie's world
// transform while re-parenting it under the pivot.
// 3. Rotate the pivot → all attached cubies orbit around it.
// 4. On completion, scene.attach(cubie) puts cubies back into the
// scene, again preserving the world transform.
// 5. Round positions & rotations to eliminate floating-point drift.
// ====================================================================
/**
* Round a cubie's local position to the nearest integer and its Euler
* rotation to the nearest multiple of 90° (PI/2). This prevents the
* cube from slowly "drifting apart" after many rotations.
*/
function roundCubieTransform(cubie) {
// Position → nearest integer (centres are always at -1, 0, or 1 × STEP)
cubie.position.x = Math.round(cubie.position.x / STEP) * STEP;
cubie.position.y = Math.round(cubie.position.y / STEP) * STEP;
cubie.position.z = Math.round(cubie.position.z / STEP) * STEP;
// Rotation → nearest 90° multiple
const snap = (v) => Math.round(v / (Math.PI / 2)) * (Math.PI / 2);
// Decompose quaternion → Euler for rounding, then write back
const euler = new THREE.Euler().setFromQuaternion(cubie.quaternion, 'XYZ');
cubie.rotation.x = snap(euler.x);
cubie.rotation.y = snap(euler.y);
cubie.rotation.z = snap(euler.z);
}
/**
* Execute one atomic rotation with a Tween animation.
* Used by the snap-to-grid phase and by the scramble sequencer.
*
* @param {string} axisStr 'x' | 'y' | 'z'
* @param {number} layerVal -1 | 0 | 1 (the layer index along the axis)
* @param {number} toAngle target angle in radians (multiple of PI/2)
* @param {number} duration ms
* @returns {Promise<void>}
*/
function executeMove(axisStr, layerVal, toAngle, duration = 180) {
return new Promise(resolve => {
const pivot = new THREE.Object3D();
scene.add(pivot);
const layerCubies = getLayerCubies(axisStr, layerVal);
if (layerCubies.length === 0) {
scene.remove(pivot);
resolve();
return;
}
// Attach — world transform is preserved, cubies now child of pivot
layerCubies.forEach(c => pivot.attach(c));
const start = { v: pivot.rotation[axisStr] };
new TWEEN.Tween(start)
.to({ v: toAngle }, duration)
.easing(TWEEN.Easing.Quadratic.InOut)
.onUpdate(() => { pivot.rotation[axisStr] = start.v; })
.onComplete(() => {
// Detach — cubies return to scene, world transform preserved
layerCubies.forEach(c => scene.attach(c));
layerCubies.forEach(roundCubieTransform);
scene.remove(pivot);
resolve();
})
.start();
});
}
// ====================================================================
// GESTURE RECOGNITION — Projection-based algorithm
//
// Overview:
// 1. On pointer-down, raycast to find the clicked cubie & face normal.
// 2. Snap the face normal to a cardinal direction (±X/±Y/±Z).
// 3. The two *other* cardinal axes are the candidate rotation axes.
// 4. For each candidate axis, compute the "tangent direction" —
// cross(axis, faceNormal) — which is the 3D direction a surface
// point moves when the layer rotates around that axis.
// 5. Project each tangent onto the 2D screen.
// 6. As the user drags, compute the 2D drag vector. Take the dot
// product with each projected tangent; the axis with the highest
// |dot| is the one the user intends to rotate.
// 7. The sign of the dot product gives the rotation direction,
// automatically correcting for back-face / top-face views.
// ====================================================================
function onPointerDown(event) {
if (isAnimating || active !== null) return;
if (event.button !== 0) return; // left-click only
// Normalised device coordinates for raycaster
pointer.x = (event.clientX / window.innerWidth) * 2 - 1;
pointer.y = -(event.clientY / window.innerHeight) * 2 + 1;
pointerStart.set(event.clientX, event.clientY);
raycaster.setFromCamera(pointer, camera);
const hits = raycaster.intersectObjects(cubies, false);
if (hits.length === 0) return; // missed all cubies
const hit = hits[0];
// --- Step 1: World-space face normal ---
// intersect.face.normal is in the geometry's *local* space.
// We transform it by the cubie's world quaternion to get world-space.
const localNormal = hit.face.normal.clone();
const worldQuat = new THREE.Quaternion();
hit.object.getWorldQuaternion(worldQuat);
const worldNormal = localNormal.applyQuaternion(worldQuat).normalize();
const faceNormal = snapToCardinal(worldNormal);
// --- Step 2: Candidate rotation axes (the two NOT equal to faceNormal) ---
const candidates = getCandidateAxes(faceNormal);
// --- Step 3: We haven't chosen an axis yet — we'll do it after the
// first meaningful drag. Store everything we need for the drag phase.
const hitPoint = hit.point.clone();
// Pre-compute tangent projections for each candidate
const screenW = renderer.domElement.clientWidth;
const screenH = renderer.domElement.clientHeight;
const candidateData = candidates.map(axisStr => {
const axis3D = AXIS_VEC[axisStr].clone();
// Tangent direction in 3D: cross(rotationAxis, faceNormal)
const tangent3D = new THREE.Vector3()
.crossVectors(axis3D, faceNormal)
.normalize();
const tangent2D = projectDirToScreen(hitPoint, tangent3D, camera, screenW, screenH);
return { axisStr, axis3D, tangent3D, tangent2D };
});
active = {
hitPoint,
faceNormal,
hitCubie: hit.object, // stored so we don't re-raycast in pointermove
candidates: candidateData,
chosen: null, // determined after first meaningful drag
pivot: null,
cubies: null,
pxPerRad: null,
startPointer: { x: event.clientX, y: event.clientY },
};
// Prevent OrbitControls from picking up this event
event.stopPropagation();
}
function onPointerMove(event) {
if (isAnimating || !active) return;
const dx = event.clientX - active.startPointer.x;
const dy = event.clientY - active.startPointer.y;
const drag2D = new THREE.Vector2(dx, dy);
const DRAG_THRESHOLD = 6; // pixels — minimum drag to commit to an axis
if (drag2D.length() < DRAG_THRESHOLD && !active.chosen) return;
// --- Axis selection (first meaningful drag) ---
if (!active.chosen) {
let bestAxis = null;
let bestDot = -Infinity;
for (const cand of active.candidates) {
// Dot product of drag direction with the projected tangent.
// The tangent whose projection best aligns with the drag wins.
const dot = Math.abs(drag2D.x * cand.tangent2D.x + drag2D.y * cand.tangent2D.y);
if (dot > bestDot) {
bestDot = dot;
bestAxis = cand;
}
}
if (!bestAxis) return;
active.chosen = bestAxis;
// --- Compute pixels-per-radian for 1:1 real-time tracking ---
// We rotate the hit-point by a tiny test angle and measure the
// screen-space displacement.
const center = new THREE.Vector3(0, 0, 0);
const relPoint = active.hitPoint.clone().sub(center);
const testAngle = 0.01; // rad
const rotated = relPoint.clone()
.applyAxisAngle(bestAxis.axis3D, testAngle)
.add(center);
const w = renderer.domElement.clientWidth;
const h = renderer.domElement.clientHeight;
const p1 = toScreen(active.hitPoint, camera, w, h);
const p2 = toScreen(rotated, camera, w, h);
active.pxPerRad = Math.max(p1.distanceTo(p2) / testAngle, 1e-4);
// --- Determine layer value from the clicked cubie's position ---
// We use the cubie reference stored during pointerdown (no re-raycast),
// because cubies may already be moving or the pointer has shifted.
const hitCubie = active.hitCubie;
const wp = new THREE.Vector3();
hitCubie.getWorldPosition(wp);
const layerVal = Math.round(wp[bestAxis.axisStr] / STEP) * STEP;
// --- Create pivot & attach layer cubies ---
const pivot = new THREE.Object3D();
scene.add(pivot);
const layerCubies = getLayerCubies(bestAxis.axisStr, layerVal);
layerCubies.forEach(c => pivot.attach(c));
active.pivot = pivot;
active.cubies = layerCubies;
active.layerVal = layerVal;
active.axisStr = bestAxis.axisStr;
active.tangent2D = bestAxis.tangent2D;
}
// --- Real-time rotation: project total drag onto the tangent ---
const dragAlongTangent = drag2D.x * active.tangent2D.x + drag2D.y * active.tangent2D.y;
const angle = dragAlongTangent / active.pxPerRad; // radians
active.pivot.rotation[active.axisStr] = angle;
}
function onPointerUp(_event) {
if (isAnimating || !active) return;
// If the user never dragged far enough, cancel.
if (!active.chosen || !active.pivot) {
active = null;
return;
}
const currentAngle = active.pivot.rotation[active.axisStr];
// --- Snap to nearest 90° multiple ---
const targetAngle = Math.round(currentAngle / (Math.PI / 2)) * (Math.PI / 2);
isAnimating = true;
document.getElementById('status').classList.add('visible');
const start = { v: currentAngle };
const axisStr = active.axisStr;
const pivot = active.pivot;
const layerCubies = active.cubies;
// Clear active *before* the tween completes so a new drag can't start
// mid-animation (isAnimating flag blocks it).
active = null;
new TWEEN.Tween(start)
.to({ v: targetAngle }, 180)
.easing(TWEEN.Easing.Quadratic.Out)
.onUpdate(() => { pivot.rotation[axisStr] = start.v; })
.onComplete(() => {
// --- Detach cubies from pivot, put back in scene ---
layerCubies.forEach(c => scene.attach(c));
layerCubies.forEach(roundCubieTransform);
scene.remove(pivot);
isAnimating = false;
document.getElementById('status').classList.remove('visible');
})
.start();
}
// ====================================================================
// SCRAMBLE — chain 20 random quarter-turn moves
// ====================================================================
async function scramble(numMoves = 20) {
if (isAnimating || active) return;
isAnimating = true;
document.getElementById('status').classList.add('visible');
const axes = ['x', 'y', 'z'];
const layers = [-STEP, 0, STEP];
const dirs = [1, -1];
// Avoid trivial back-and-forth: track the last move
let last = { axis: '', layer: 0 };
for (let i = 0; i < numMoves; i++) {
let axis, layer, dir;
do {
axis = axes[Math.floor(Math.random() * 3)];
layer = layers[Math.floor(Math.random() * 3)];
dir = dirs[Math.floor(Math.random() * 2)];
} while (
// Skip moves that exactly undo the previous one
axis === last.axis &&
Math.abs(layer - last.layer) < EPSILON &&
i > 0
);
last = { axis, layer };
await executeMove(axis, layer, dir * Math.PI / 2, 100);
}
isAnimating = false;
document.getElementById('status').classList.remove('visible');
}
// ====================================================================
// RESET — rebuild the entire cube from scratch
// ====================================================================
function resetCube() {
if (isAnimating || active) return;
// Remove all existing cubies from the scene.
// NOTE: we do NOT dispose materials — they are shared across all
// cubies and will be reused by the rebuilt meshes.
cubies.forEach(c => {
if (c.parent) c.parent.remove(c);
});
cubies.length = 0;
// Rebuild the 3×3×3 grid at identity rotation
for (let gx = -1; gx <= 1; gx++) {
for (let gy = -1; gy <= 1; gy++) {
for (let gz = -1; gz <= 1; gz++) {
const materials = [
gx === 1 ? matRight : matBlack,
gx === -1 ? matLeft : matBlack,
gy === 1 ? matUp : matBlack,
gy === -1 ? matDown : matBlack,
gz === 1 ? matFront : matBlack,
gz === -1 ? matBack : matBlack,
];
const cubie = new THREE.Mesh(cubieGeo, materials);
cubie.position.set(gx * STEP, gy * STEP, gz * STEP);
cubie.castShadow = true;
cubie.receiveShadow = true;
cubie.userData = { initGrid: { x: gx, y: gy, z: gz } };
scene.add(cubie);
cubies.push(cubie);
}
}
}
}
// ====================================================================
// EVENT BINDING
// ====================================================================
renderer.domElement.addEventListener('pointerdown', onPointerDown);
window.addEventListener('pointermove', onPointerMove);
window.addEventListener('pointerup', onPointerUp);
// If the pointer leaves the window mid-drag, cancel
window.addEventListener('pointerleave', (e) => {
if (active && !isAnimating) {
// Trigger snap by simulating pointerup
onPointerUp(e);
}
});
document.getElementById('scramble').addEventListener('click', () => scramble(20));
document.getElementById('reset').addEventListener('click', resetCube);
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});
// Keyboard shortcuts
window.addEventListener('keydown', (e) => {
if (e.key === 's' || e.key === 'S') scramble(20);
if (e.key === 'r' || e.key === 'R') resetCube();
});
// ====================================================================
// RENDER LOOP
// ====================================================================
function animate(timestamp) {
requestAnimationFrame(animate);
TWEEN.update(timestamp);
controls.update();
renderer.render(scene, camera);
}
requestAnimationFrame(animate);
// ====================================================================
// STARTUP LOG
// ====================================================================
console.log('%c🧊 Rubik\'s Cube Ready %c| %cLeft-drag%c rotate layer %c· %cRight-drag%c orbit %c· %cScroll%c zoom %c· %cS%c scramble %c· %cR%c reset',
'font-size:16px;font-weight:bold;',
'',
'color:#e94560;', '', 'color:#666;',
'color:#4a9eff;', '', 'color:#666;',
'color:#ffd500;', '', 'color:#666;',
'color:#e94560;', '', 'color:#666;',
'color:#4a9eff;', '');
</script>
</body>
</html>
前言
备受期待的DeepSeek V4终于发布了,相信很多人都想要看到这个模型在Coding任务中的实际表现。按传统来说,我们应该跑一个天气卡片,但是我一直觉得只有天气卡片完全反映不出来模型真正的能力,于是我决定来点创新的实战测试。
为保证本测试接近实际使用场景,测试全程使用Claude Code v2.1.86,官方正价API,模型映射配置如下所示:
图片862×234 16 KB
[!NOTE]
本次测试中DeepSeek V4所编写的源码可在 Raven95676/ds_v4_test: DeepSeek V4 Test - Codeberg.org 获取。真实环境测试仅保留Diff文件。由于时间以及精力还有财力限制,测试可能片面,欢迎各位补充测试用例。
综上,让我们开始测试吧!
希望测完了我不会变成负翁
独立项目从零生成测试
很多情况下,我们有了灵感都会让AI来开荒。我们首先来测试AI从零生成的能力。
天气卡片
首先是我们最为经典的天气卡片。
Prompt:
请编写一个单文件的HTML,实现一个现代化的天气卡片应用。所有的CSS和JS必须内联在这个HTML文件中,不允许引入外部的UI组件库。界面设计要求具备毛玻璃效果,包含天气图标、温度、地点、湿度、风速等元素,排版要求高水准的美感。
成果展示:
图片709×672 52.6 KB
尽管没有要求,DeepSeek V4依然接入了实际可用的数据源,填入OpenWeatherMap API Key后:
图片647×623 41.2 KB
算法可视化
[!NOTE]
本项测试使用npm create vite@latest的TypeScript+React+React Compiler模板创建了一个空项目,DeepSeek V4将在这个空项目的基础上开发。本项测试无额外预装依赖。
Prompt:
使用React+TypeScript编写一个算法可视化网站,包含排序算法可视化和寻路算法可视化两个模块,通过顶部导航标签页切换。使用纯React状态与CSS实现,不依赖任何第三方动画库或状态管理库。具体要求如下:
## 排序算法可视化(标签页1)
1. 随机生成包含20个整数的数组,用垂直柱状图展示。
2. 提供下拉菜单选择算法:冒泡排序、选择排序、插入排序、快速排序。
3. 排序过程逐步动画展示。
## 寻路算法可视化(标签页2)
1. 显示一个20×20的网格。允许用户交互编辑网格。
2. 提供下拉菜单选择寻路算法:Dijkstra、A*、BFS、DFS。
3. 寻路过程逐步动画展示。
成果展示:
图片1300×629 15.5 KB
图片741×831 14.8 KB
3D三角形旋转
[!NOTE]
本项测试使用npm create vite@latest的TypeScript+React+React Compiler模板创建了一个空项目,DeepSeek V4将在这个空项目的基础上开发。本项测试额外预装依赖three @react-three/fiber @react-three/drei @types/three。
Prompt:
使用React+TypeScript+React Three Fiber+Three.js编写一个3D场景。场景要求:
1. 场景中央显示一个彩色的3D等边三角形(红、绿、蓝),背景颜色为深灰色,带有简单的网格地面辅助线。
2. 三角形绕自身Y轴以恒定速度旋转。
3. 允许用户通过鼠标拖拽旋转整个场景视角,支持滚轮缩放。
成果展示:
图片598×645 28.3 KB
爬虫编写
[!NOTE]
本项测试使用uv init创建了一个空项目,预装依赖playwright,启用chrome-devtoolsMCP,选取盗版小说网站https://m.bqgl.cc/look/7546/作为评估示例。测试行为严格限于非商业性的技术验证,且遵循低频请求原则。如相关权利人认为该示例侵犯了网站的合法权益,请与我联系。
Prompt:
此项目为Python爬虫,目标网址为https://m.snapd.net/read/165986/。请使用chrome-devtools
MCP分析目标网站并完成项目,依赖管理使用uv,使用playwright进行抓取。有以下要求:
1. 解析出小说的所有章节列表,提取章节正文内容,清理多余的HTML标签,清理非小说正文的广告内容,保留纯文本段落。
2. 将所有章节按顺序合并保存到一个txt文件中,每章标题作为一行,空一行,接着是正文内容,然后两个空行分隔下一章。
3. 请求之间设置1秒的延迟,实现基本的错误处理和重试机制。
成果展示:
图片926×909 111 KB
戴森云模拟
[!NOTE]
本项测试使用npm create vite@latest的TypeScript+React+React Compiler模板创建了一个空项目,DeepSeek V4将在这个空项目的基础上开发。本项测试额外预装依赖three @react-three/fiber @react-three/drei @types/three,启用chrome-devtoolsMCP。
Prompt:
使用React+TypeScript+React Three Fiber+Three.js编写一个戴森云模拟场景。场景要求:
1. 场景中心有一颗发光恒星,发出暖黄色光芒,并带有光晕效果。
2. 恒星周围分布着大量小型蓝白色发光粒子,它们大致位于一个球壳范围内,形成不规则的云状结构。所有粒子以不同的角速度绕恒星中心公转,且有小幅度的随机径向偏移运动。
3. 允许用户通过鼠标拖拽旋转整个场景视角,支持滚轮缩放,右上角显示FPS等数据。
4. 添加基本的星场背景。
5. 小型蓝白色发光粒子需要允许用户通过滑块调整数量,数量范围在50-10000,请在不减少粒子数量的情况下保证性能。
请使用chrome-devtools MCP对成果进行验证。
成果展示:
screenshotfinal929×865 95.7 KB
真实环境测试
当然,我们不仅需要AI能够从零开荒,修复已有问题的能力也是很重要的。接下来我们测试DeepSeek V4在真实环境中的表现。
这个测试是选择一个已被修复的issue,然后分析AI是如何对这个问题进行修复的。
[!NOTE]
在实际应用中,不建议向开源项目贡献纯AI生成代码,尤其是在开源项目明确拒绝的情况下。
cpython
Crash on _ssl__SSLContext_load_cert_chain_impl (requests running w/ cert in multi-threading)
已打开 09:16AM - 26 May 25 UTC 已关闭 01:27PM - 08 Oct 25 UTC Conobi extension-modules type-crash topic-SSL# Crash report ### What happened? Hi. We've been investigating random crashes …of our FastAPI application for over 6 months, and we think we've found the culprit. When **calling requests with a custom cert (like in the code below) in a multi-threaded paradigm**, it can crashes. On some versions, like 3.12/3.13, it can in some case even block Python in a zombie state, where the process isn't killed but keep being hung. I tested on all the versions mentionned, and **the crash happened on all versions**. In the latest ones (>=3.12), it feels like I get more often double free. ```python import threading from concurrent.futures import ThreadPoolExecutor from datetime import datetime, timedelta from pathlib import Path import requests from cryptography import x509 from cryptography.hazmat.primitives import hashes, serialization from cryptography.hazmat.primitives.asymmetric import rsa from cryptography.hazmat.primitives.serialization import pkcs12 from cryptography.x509.oid import NameOID CERT_PEM = "client_cert.pem" KEY_PEM = "client_key.pem" PFX_FILE = "client_cert.pfx" CERT_PASSWORD = b"password" # For PFX export def generate_and_save_cert() -> None: """Generate RSA key and self-signed cert, save PEM and PFX. Can be commented out. """ key = rsa.generate_private_key( public_exponent=65537, key_size=2048, ) subject = issuer = x509.Name( [ x509.NameAttribute(NameOID.COUNTRY_NAME, "US"), x509.NameAttribute(NameOID.STATE_OR_PROVINCE_NAME, "California"), x509.NameAttribute(NameOID.LOCALITY_NAME, "San Francisco"), x509.NameAttribute(NameOID.ORGANIZATION_NAME, "Test Org"), x509.NameAttribute(NameOID.COMMON_NAME, "localhost"), ] ) cert = ( x509.CertificateBuilder() .subject_name(subject) .issuer_name(issuer) .public_key(key.public_key()) .serial_number(x509.random_serial_number()) .not_valid_before( datetime.now( datetime.utcnow() # for backward compatibility ) ) .not_valid_after( datetime.now( datetime.utcnow() # for backward compatibility ) + timedelta(days=365) ) .sign(key, hashes.SHA256()) ) Path(CERT_PEM).write_bytes(cert.public_bytes(serialization.Encoding.PEM)) Path(KEY_PEM).write_bytes( key.private_bytes( encoding=serialization.Encoding.PEM, format=serialization.PrivateFormat.TraditionalOpenSSL, encryption_algorithm=serialization.NoEncryption(), ) ) pfx = pkcs12.serialize_key_and_certificates( name=b"client", key=key, cert=cert, cas=None, encryption_algorithm=serialization.BestAvailableEncryption(CERT_PASSWORD), ) Path(PFX_FILE).write_bytes(pfx) def post_with_cert(url: str, idx: int) -> None: """Perform a POST request using PEM cert.""" try: response = requests.get( url, data={"test": f"thread-{idx}"}, cert=(CERT_PEM, KEY_PEM), # PEM files timeout=10, ) print(f"Thread {idx}: Status {response.status_code}") except Exception as exc: print(f"Thread {idx}: Exception {exc}") def main() -> None: generate_and_save_cert() url = "https://example.com" # <-- Change this! with ThreadPoolExecutor(max_workers=150) as executor: for i in range(150): executor.submit(post_with_cert, url, i) if __name__ == "__main__": main() ``` Here's the crash dump: ``` Core was generated by `/usr/local/bin/python3.15 foo.py'. Program terminated with signal SIGABRT, Aborted. #0 0x0000771454ecf624 in ?? () from /usr/lib/libc.so.6 [Current thread is 1 (Thread 0x7713720006c0 (LWP 87696))] #0 0x0000771454ecf624 in ?? () from /usr/lib/libc.so.6 #1 0x0000771454e75ba0 in raise () from /usr/lib/libc.so.6 #2 0x0000771454e5d582 in abort () from /usr/lib/libc.so.6 #3 0x0000771454e5e3bf in ?? () from /usr/lib/libc.so.6 #4 0x0000771454ed9765 in ?? () from /usr/lib/libc.so.6 #5 0x0000771454edbc8a in ?? () from /usr/lib/libc.so.6 #6 0x0000771454ede9ab in free () from /usr/lib/libc.so.6 #7 0x0000771453dc2eb5 in RSA_free () from /usr/lib/libcrypto.so.3 #8 0x0000771453d5ebd2 in ?? () from /usr/lib/libcrypto.so.3 #9 0x0000771453d5f498 in EVP_PKEY_free () from /usr/lib/libcrypto.so.3 #10 0x0000771454198e8a in ?? () from /usr/lib/libssl.so.3 #11 0x000077145419e4d6 in SSL_CTX_use_PrivateKey_file () from /usr/lib/libssl.so.3 #12 0x000077145429f899 in _ssl__SSLContext_load_cert_chain_impl (self=0x7714536f9b50, certfile=<optimized out>, keyfile=<optimized out>, password=<optimized out>) at ./Modules/_ssl.c:4148 #13 _ssl__SSLContext_load_cert_chain (self=0x7714536f9b50, args=<optimized out>, nargs=<optimized out>, kwnames=<optimized out>) at ./Modules/clinic/_ssl.c.h:1429 #14 0x0000586280553ca0 in _PyObject_VectorcallTstate (tstate=0x58629578de30, callable=0x771454583ba0, args=0x7714540e89e0, nargsf=<optimized out>, kwnames=0x0) at ./Include/internal/pycore_call.h:169 #15 PyObject_Vectorcall (callable=0x771454583ba0, args=args@entry=0x771371ffe468, nargsf=<optimized out>, kwnames=kwnames@entry=0x0) at Objects/call.c:327 #16 0x00005862806cda53 in _PyEval_EvalFrameDefault (tstate=0x58629578de30, frame=<optimized out>, throwflag=<optimized out>) at Python/generated_cases.c.h:1619 #17 0x00005862806d9efc in _PyEval_EvalFrame (tstate=0x58629578de30, frame=<optimized out>, throwflag=0) at ./Include/internal/pycore_ceval.h:119 #18 _PyEval_Vector (tstate=0x58629578de30, func=0x7714538ee090, locals=0x0, args=0x771452165530, argcount=<optimized out>, kwnames=<optimized out>) at Python/ceval.c:1961 #19 0x0000586280557fc2 in _PyObject_VectorcallTstate (tstate=0x58629578de30, callable=0x7714538ee090, args=0x771452165530, nargsf=4, kwnames=0x7714521800b0) at ./Include/internal/pycore_call.h:169 #20 method_vectorcall (method=<optimized out>, args=0x771452165538, nargsf=<optimized out>, kwnames=0x7714521800b0) at Objects/classobject.c:64 #21 0x0000586280555b98 in _PyVectorcall_Call (tstate=0x58629578de30, func=0x586280557e30 <method_vectorcall>, callable=0x77145217da00, tuple=<optimized out>, kwargs=<optimized out>) at Objects/call.c:285 #22 _PyObject_Call (tstate=0x58629578de30, callable=0x77145217da00, args=<optimized out>, kwargs=<optimized out>) at Objects/call.c:348 #23 PyObject_Call (callable=0x77145217da00, args=<optimized out>, kwargs=<optimized out>) at Objects/call.c:373 #24 0x00005862806cde12 in _PyEval_EvalFrameDefault (tstate=0x58629578de30, frame=<optimized out>, throwflag=<optimized out>) at Python/generated_cases.c.h:2654 #25 0x00005862806d9efc in _PyEval_EvalFrame (tstate=0x58629578de30, frame=<optimized out>, throwflag=0) at ./Include/internal/pycore_ceval.h:119 #26 _PyEval_Vector (tstate=0x58629578de30, func=0x77145361b1c0, locals=0x0, args=0x771452157730, argcount=<optimized out>, kwnames=<optimized out>) at Python/ceval.c:1961 #27 0x0000586280557fc2 in _PyObject_VectorcallTstate (tstate=0x58629578de30, callable=0x77145361b1c0, args=0x771452157730, nargsf=2, kwnames=0x771452164160) at ./Include/internal/pycore_call.h:169 #28 method_vectorcall (method=<optimized out>, args=0x771452157738, nargsf=<optimized out>, kwnames=0x771452164160) at Objects/classobject.c:64 #29 0x0000586280555b98 in _PyVectorcall_Call (tstate=0x58629578de30, func=0x586280557e30 <method_vectorcall>, callable=0x771452157680, tuple=<optimized out>, kwargs=<optimized out>) at Objects/call.c:285 #30 _PyObject_Call (tstate=0x58629578de30, callable=0x771452157680, args=<optimized out>, kwargs=<optimized out>) at Objects/call.c:348 #31 PyObject_Call (callable=0x771452157680, args=<optimized out>, kwargs=<optimized out>) at Objects/call.c:373 #32 0x00005862806cde12 in _PyEval_EvalFrameDefault (tstate=0x58629578de30, frame=<optimized out>, throwflag=<optimized out>) at Python/generated_cases.c.h:2654 #33 0x00005862806d9efc in _PyEval_EvalFrame (tstate=0x58629578de30, frame=<optimized out>, throwflag=0) at ./Include/internal/pycore_ceval.h:119 #34 _PyEval_Vector (tstate=0x58629578de30, func=0x77145361be20, locals=0x0, args=0x771452157b70, argcount=<optimized out>, kwnames=<optimized out>) at Python/ceval.c:1961 #35 0x0000586280557fc2 in _PyObject_VectorcallTstate (tstate=0x58629578de30, callable=0x77145361be20, args=0x771452157b70, nargsf=2, kwnames=0x771452164700) at ./Include/internal/pycore_call.h:169 #36 method_vectorcall (method=<optimized out>, args=0x771452157b78, nargsf=<optimized out>, kwnames=0x771452164700) at Objects/classobject.c:64 #37 0x0000586280555b98 in _PyVectorcall_Call (tstate=0x58629578de30, func=0x586280557e30 <method_vectorcall>, callable=0x771452156b80, tuple=<optimized out>, kwargs=<optimized out>) at Objects/call.c:285 #38 _PyObject_Call (tstate=0x58629578de30, callable=0x771452156b80, args=<optimized out>, kwargs=<optimized out>) at Objects/call.c:348 #39 PyObject_Call (callable=0x771452156b80, args=<optimized out>, kwargs=<optimized out>) at Objects/call.c:373 #40 0x00005862806cde12 in _PyEval_EvalFrameDefault (tstate=0x58629578de30, frame=<optimized out>, throwflag=<optimized out>) at Python/generated_cases.c.h:2654 #41 0x00005862806d9efc in _PyEval_EvalFrame (tstate=0x58629578de30, frame=<optimized out>, throwflag=0) at ./Include/internal/pycore_ceval.h:119 #42 _PyEval_Vector (tstate=0x58629578de30, func=0x77145361b8a0, locals=0x0, args=0x7714521553f0, argcount=<optimized out>, kwnames=<optimized out>) at Python/ceval.c:1961 #43 0x0000586280557fc2 in _PyObject_VectorcallTstate (tstate=0x58629578de30, callable=0x77145361b8a0, args=0x7714521553f0, nargsf=1, kwnames=0x771452127d60) at ./Include/internal/pycore_call.h:169 #44 method_vectorcall (method=<optimized out>, args=0x7714521553f8, nargsf=<optimized out>, kwnames=0x771452127d60) at Objects/classobject.c:64 #45 0x0000586280555b98 in _PyVectorcall_Call (tstate=0x58629578de30, func=0x586280557e30 <method_vectorcall>, callable=0x771452154c40, tuple=<optimized out>, kwargs=<optimized out>) at Objects/call.c:285 #46 _PyObject_Call (tstate=0x58629578de30, callable=0x771452154c40, args=<optimized out>, kwargs=<optimized out>) at Objects/call.c:348 #47 PyObject_Call (callable=0x771452154c40, args=<optimized out>, kwargs=<optimized out>) at Objects/call.c:373 #48 0x00005862806cde12 in _PyEval_EvalFrameDefault (tstate=0x58629578de30, frame=<optimized out>, throwflag=<optimized out>) at Python/generated_cases.c.h:2654 #49 0x00005862806d9efc in _PyEval_EvalFrame (tstate=0x58629578de30, frame=<optimized out>, throwflag=0) at ./Include/internal/pycore_ceval.h:119 #50 _PyEval_Vector (tstate=0x58629578de30, func=0x7714549d1170, locals=0x0, args=0x771371fff938, argcount=<optimized out>, kwnames=<optimized out>) at Python/ceval.c:1961 #51 0x000058628055802b in _PyObject_VectorcallTstate (tstate=0x58629578de30, callable=0x7714549d1170, args=0x771371fff938, nargsf=1, kwnames=0x0) at ./Include/internal/pycore_call.h:169 #52 method_vectorcall (method=<optimized out>, args=0x771371fffbd8, nargsf=<optimized out>, kwnames=0x0) at Objects/classobject.c:72 #53 0x00005862806f784c in _PyObject_VectorcallTstate (tstate=0x58629578de30, callable=0x771452154800, args=0x771371fffbd8, nargsf=<optimized out>, kwnames=0x0) at ./Include/internal/pycore_call.h:169 #54 context_run (self=0x7714521549c0, args=0x771371fffbd0, nargs=<optimized out>, kwnames=0x0) at Python/context.c:728 #55 0x00005862806ceefe in _PyEval_EvalFrameDefault (tstate=0x58629578de30, frame=<optimized out>, throwflag=<optimized out>) at Python/generated_cases.c.h:3764 #56 0x00005862806d9efc in _PyEval_EvalFrame (tstate=0x58629578de30, frame=<optimized out>, throwflag=0) at ./Include/internal/pycore_ceval.h:119 #57 _PyEval_Vector (tstate=0x58629578de30, func=0x7714549d1220, locals=0x0, args=0x771371fffdd8, argcount=<optimized out>, kwnames=<optimized out>) at Python/ceval.c:1961 #58 0x000058628055802b in _PyObject_VectorcallTstate (tstate=0x58629578de30, callable=0x7714549d1220, args=0x771371fffdd8, nargsf=1, kwnames=0x0) at ./Include/internal/pycore_call.h:169 #59 method_vectorcall (method=<optimized out>, args=0x586280a26958 <_PyRuntime+90104>, nargsf=<optimized out>, kwnames=0x0) at Objects/classobject.c:72 #60 0x00005862807ed50c in thread_run (boot_raw=0x58629578ddf0) at ./Modules/_threadmodule.c:368 #61 0x000058628076d957 in pythread_wrapper (arg=<optimized out>) at Python/thread_pthread.h:242 #62 0x0000771454ecd70a in ?? () from /usr/lib/libc.so.6 #63 0x0000771454f51aac in ?? () from /usr/lib/libc.so.6 ``` Since I did run the test on multiple versions: - I did run the test on official Docker Python images (except 3.9/3.10/3.14/3.15) - My Docker `openssl version`: `OpenSSL 3.0.16 11 Feb 2025 (Library: OpenSSL 3.0.16 11 Feb 2025)` - My host `openssl version`: `OpenSSL 3.4.1 11 Feb 2025 (Library: OpenSSL 3.4.1 11 Feb 2025)` - For CPython main branch, here's my `python -VV`: `Python 3.15.0a0 (heads/main-dirty:1729468016, May 23 2025, 14:52:55) [GCC 14.2.1 20250207]` - My distrib: `Manjaro Linux 25.0.0` - `/proc/version`: `Linux version 6.11.5-lqx1-1-lqx (linux-lqx@archlinux) (gcc (GCC) 14.2.1 20240910, GNU ld (GNU Binutils) 2.43.0) #1 ZEN SMP PREEMPT Tue, 22 Oct 2024 15:40:56 +0000` ### CPython versions tested on: 3.10, 3.11, 3.12, 3.13, 3.14, CPython main branch, 3.9 ### Operating systems tested on: Linux ### Output from running 'python -VV' on the command line: _No response_ ### Linked PRs * gh-134724 * gh-137107 * gh-137126
Prompt:Issue原文
成果分析:
总体来说就是头痛医头,脚痛医脚。DeepSeek V4确实可以找到问题并应用修复,但是它只会在一个点上进行修复,修复并不完整,缺乏全局视角。考虑到大多数LLM都有这个问题,并且为了公平起见毫无上下文引导,我个人认为倒是可以给出一个合格的成绩,不过距离真的生产可用还是有一段距离的。
网友解答:--【壹】--:
魔方花了我三块~!!!!
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>3x3 Rubik's Cube — Physics Grade</title>
<style>
:root {
--bg: #1a1a2e;
--surface: #16213e;
--accent: #e94560;
--text: #eaeaea;
--muted: rgba(255,255,255,0.45);
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
overflow: hidden;
background: var(--bg);
font-family: 'Inter', 'Segoe UI', system-ui, sans-serif;
user-select: none;
-webkit-user-select: none;
-webkit-tap-highlight-color: transparent;
height: 100dvh;
width: 100vw;
}
#canvas {
display: block;
position: fixed;
inset: 0;
}
#ui {
position: fixed;
bottom: 36px;
left: 50%;
transform: translateX(-50%);
display: flex;
gap: 14px;
z-index: 10;
}
#ui button {
padding: 13px 30px;
font-size: 15px;
font-weight: 600;
letter-spacing: 0.04em;
border: none;
border-radius: 10px;
cursor: pointer;
transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
outline: none;
position: relative;
overflow: hidden;
}
#scramble {
background: rgba(233, 69, 96, 0.85);
color: #fff;
box-shadow: 0 4px 24px rgba(233, 69, 96, 0.35);
}
#scramble:hover {
background: rgba(255, 90, 120, 0.95);
box-shadow: 0 6px 32px rgba(233, 69, 96, 0.5);
transform: translateY(-2px);
}
#reset {
background: rgba(22, 33, 62, 0.85);
color: #fff;
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.25);
}
#reset:hover {
background: rgba(30, 45, 80, 0.95);
box-shadow: 0 6px 32px rgba(0, 0, 0, 0.4);
transform: translateY(-2px);
}
button:active { transform: scale(0.96) !important; }
#info {
position: fixed;
bottom: 100px;
left: 50%;
transform: translateX(-50%);
color: var(--muted);
font-size: 13px;
z-index: 10;
pointer-events: none;
text-align: center;
letter-spacing: 0.03em;
transition: opacity 0.5s;
}
#status {
position: fixed;
top: 30px;
left: 50%;
transform: translateX(-50%);
color: var(--muted);
font-size: 12px;
z-index: 10;
pointer-events: none;
letter-spacing: 0.05em;
text-transform: uppercase;
opacity: 0;
transition: opacity 0.3s;
}
#status.visible { opacity: 1; }
</style>
</head>
<body>
<canvas id="canvas"></canvas>
<div id="status">Rotating...</div>
<div id="info">
<span style="opacity:0.8">Left-drag</span> rotate layer ·
<span style="opacity:0.8">Right-drag</span> orbit ·
<span style="opacity:0.8">Scroll</span> zoom
</div>
<div id="ui">
<button id="scramble">Scramble</button>
<button id="reset">Reset</button>
</div>
<!-- ==================== IMPORT MAP ==================== -->
<script type="importmap">
{
"imports": {
"three": "https://unpkg.com/three@0.160.0/build/three.module.js",
"three/addons/": "https://unpkg.com/three@0.160.0/examples/jsm/",
"@tweenjs/tween.js": "https://unpkg.com/@tweenjs/tween.js@18.6.4/dist/tween.esm.js"
}
}
</script>
<script type="module">
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
import * as TWEEN from '@tweenjs/tween.js';
// ====================================================================
// CONSTANTS & CONFIGURATION
// ====================================================================
const CUBIE_SIZE = 0.85; // edge length of each small cube
const SPACING = 0.15; // visible gap between cubies
const STEP = 1.0; // center-to-center distance (= CUBIE_SIZE + SPACING)
const EPSILON = 0.15; // threshold for layer membership check
const DRAG_SCALE = 1.0; // 1:1 tracking multiplier (computed dynamically)
// Standard Rubik's cube face colours (Western scheme)
const FACE_COLORS = {
right: '#C41E3A', // Red — +X
left: '#FF5800', // Orange — -X
up: '#FFFFFF', // White — +Y
down: '#FFD500', // Yellow — -Y
front: '#009E60', // Green — +Z
back: '#0051BA', // Blue — -Z
};
// Axes as 3D vectors for math operations
const AXIS_VEC = {
x: new THREE.Vector3(1, 0, 0),
y: new THREE.Vector3(0, 1, 0),
z: new THREE.Vector3(0, 0, 1),
};
// ====================================================================
// CANVAS TEXTURE GENERATOR (programmatic, zero external assets)
// ====================================================================
/**
* Draw a rounded-rectangle path on a 2D canvas context.
* Simulates the plastic sticker with chamfered corners.
*/
function roundedRect(ctx, x, y, w, h, r) {
ctx.beginPath();
ctx.moveTo(x + r, y);
ctx.lineTo(x + w - r, y);
ctx.arcTo(x + w, y, x + w, y + r, r);
ctx.lineTo(x + w, y + h - r);
ctx.arcTo(x + w, y + h, x + w - r, y + h, r);
ctx.lineTo(x + r, y + h);
ctx.arcTo(x, y + h, x, y + r, r);
ctx.lineTo(x, y + r);
ctx.arcTo(x, y, x + r, y, r);
ctx.closePath();
}
/**
* Create a CanvasTexture for one cubie face.
* @param {string|null} color Hex colour of the sticker, or null for black plastic.
* @param {number} size Texture resolution (square).
* @returns {THREE.CanvasTexture}
*/
function createFaceTexture(color, size = 256) {
const canvas = document.createElement('canvas');
canvas.width = size;
canvas.height = size;
const ctx = canvas.getContext('2d');
// --- Black plastic base (visible at edges / chamfer) ---
ctx.fillStyle = '#121212';
ctx.fillRect(0, 0, size, size);
if (color) {
const margin = size * 0.14; // black border width
const radius = size * 0.10; // corner radius
const x = margin;
const y = margin;
const w = size - 2 * margin;
const h = size - 2 * margin;
// --- Coloured sticker with rounded corners ---
ctx.fillStyle = color;
roundedRect(ctx, x, y, w, h, radius);
ctx.fill();
// --- Soft highlight gradient (top → bottom) for gloss ---
const grad = ctx.createLinearGradient(x, y, x, y + h);
grad.addColorStop(0, 'rgba(255,255,255,0.28)');
grad.addColorStop(0.35, 'rgba(255,255,255,0.04)');
grad.addColorStop(0.65, 'rgba(0,0,0,0.02)');
grad.addColorStop(1, 'rgba(0,0,0,0.18)');
ctx.fillStyle = grad;
roundedRect(ctx, x, y, w, h, radius);
ctx.fill();
// --- Subtle inner border (sticker edge) ---
ctx.strokeStyle = 'rgba(0,0,0,0.25)';
ctx.lineWidth = size * 0.012;
roundedRect(ctx, x, y, w, h, radius);
ctx.stroke();
}
const texture = new THREE.CanvasTexture(canvas);
texture.colorSpace = THREE.SRGBColorSpace;
texture.minFilter = THREE.LinearMipmapLinearFilter;
texture.magFilter = THREE.LinearFilter;
texture.generateMipmaps = true;
return texture;
}
// ====================================================================
// SCENE, RENDERER, CAMERA, LIGHTING
// ====================================================================
const canvas = document.getElementById('canvas');
const renderer = new THREE.WebGLRenderer({ canvas, antialias: true, alpha: false });
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure = 1.1;
renderer.outputColorSpace = THREE.SRGBColorSpace;
const scene = new THREE.Scene();
scene.background = new THREE.Color('#1a1a2e');
scene.fog = new THREE.Fog('#1a1a2e', 8, 30);
const camera = new THREE.PerspectiveCamera(
42, // FOV
window.innerWidth / window.innerHeight, // aspect
0.5, // near
40 // far
);
camera.position.set(5.5, 4.2, 6.8);
camera.lookAt(0, 0, 0);
// --- Ambient light (fills shadows, prevents pure black) ---
const ambient = new THREE.AmbientLight('#bcc8e0', 1.6);
scene.add(ambient);
// --- Hemisphere light (sky / ground colour gradient) ---
const hemi = new THREE.HemisphereLight('#8899cc', '#332244', 0.6);
scene.add(hemi);
// --- Key directional light (casts shadows) ---
const keyLight = new THREE.DirectionalLight('#ffffff', 4.5);
keyLight.position.set(7, 14, 5);
keyLight.castShadow = true;
keyLight.shadow.mapSize.width = 2048;
keyLight.shadow.mapSize.height = 2048;
keyLight.shadow.camera.near = 0.5;
keyLight.shadow.camera.far = 60;
keyLight.shadow.camera.left = -8;
keyLight.shadow.camera.right = 8;
keyLight.shadow.camera.top = 8;
keyLight.shadow.camera.bottom = -8;
keyLight.shadow.bias = -0.0004;
keyLight.shadow.normalBias = 0.02;
scene.add(keyLight);
// --- Fill light (opposite side, softer, no shadow) ---
const fillLight = new THREE.DirectionalLight('#8899cc', 1.2);
fillLight.position.set(-3, 2, -4);
scene.add(fillLight);
// ====================================================================
// GROUND PLANE (shadow receiver — enhances depth perception)
// ====================================================================
const groundGeo = new THREE.PlaneGeometry(20, 20);
const groundMat = new THREE.MeshStandardMaterial({
color: '#1a1a2e',
roughness: 0.9,
metalness: 0.0,
});
const ground = new THREE.Mesh(groundGeo, groundMat);
ground.rotation.x = -Math.PI / 2;
ground.position.y = -3.5;
ground.receiveShadow = true;
scene.add(ground);
// ====================================================================
// CUBE CONSTRUCTION — 27 independent cubies
// ====================================================================
/** @type {THREE.Mesh[]} */
const cubies = [];
// Pre-generate shared textures (7 total — 6 colours + 1 black)
const texBlack = createFaceTexture(null);
const texRight = createFaceTexture(FACE_COLORS.right);
const texLeft = createFaceTexture(FACE_COLORS.left);
const texUp = createFaceTexture(FACE_COLORS.up);
const texDown = createFaceTexture(FACE_COLORS.down);
const texFront = createFaceTexture(FACE_COLORS.front);
const texBack = createFaceTexture(FACE_COLORS.back);
// Shared materials (reused across cubies for efficiency)
function makeMat(tex, rough = 0.35, metal = 0.02) {
return new THREE.MeshStandardMaterial({
map: tex,
roughness: rough,
metalness: metal,
});
}
const matBlack = makeMat(texBlack, 0.55, 0.0);
const matRight = makeMat(texRight);
const matLeft = makeMat(texLeft);
const matUp = makeMat(texUp);
const matDown = makeMat(texDown);
const matFront = makeMat(texFront);
const matBack = makeMat(texBack);
const cubieGeo = new THREE.BoxGeometry(CUBIE_SIZE, CUBIE_SIZE, CUBIE_SIZE, 1, 1, 1);
// Build the 3×3×3 grid
for (let gx = -1; gx <= 1; gx++) {
for (let gy = -1; gy <= 1; gy++) {
for (let gz = -1; gz <= 1; gz++) {
// Only outer faces get a coloured sticker; inner faces are black.
// BoxGeometry material order: [+X, -X, +Y, -Y, +Z, -Z]
const materials = [
gx === 1 ? matRight : matBlack, // +X
gx === -1 ? matLeft : matBlack, // -X
gy === 1 ? matUp : matBlack, // +Y
gy === -1 ? matDown : matBlack, // -Y
gz === 1 ? matFront : matBlack, // +Z
gz === -1 ? matBack : matBlack, // -Z
];
const cubie = new THREE.Mesh(cubieGeo, materials);
cubie.position.set(gx * STEP, gy * STEP, gz * STEP);
cubie.castShadow = true;
cubie.receiveShadow = true;
// Store initial grid index for potential use
cubie.userData = { initGrid: { x: gx, y: gy, z: gz } };
scene.add(cubie);
cubies.push(cubie);
}
}
}
// ====================================================================
// ORBIT CONTROLS (right-click drag to orbit; scroll to zoom)
// ====================================================================
const controls = new OrbitControls(camera, renderer.domElement);
controls.target.set(0, 0, 0);
controls.enableDamping = true;
controls.dampingFactor = 0.12;
controls.minDistance = 4.0;
controls.maxDistance = 14.0;
controls.maxPolarAngle = Math.PI * 0.72; // prevent going under the ground
// Disable left-click so it never conflicts with our layer-rotation gesture.
// OrbitControls reads `mouseButtons.LEFT` and skips if the value is not a
// recognised MOUSE button constant (we use -1 to guarantee a no-match).
controls.mouseButtons = {
LEFT: -1, // disabled
MIDDLE: THREE.MOUSE.DOLLY,
RIGHT: THREE.MOUSE.ROTATE,
};
controls.update();
// Prevent the browser context menu on right-click over the canvas
renderer.domElement.addEventListener('contextmenu', e => e.preventDefault());
// ====================================================================
// INTERACTION STATE MACHINE
// ====================================================================
const raycaster = new THREE.Raycaster();
raycaster.far = 12;
/** One-shot state for an active finger-drag rotation */
let active = null; // { pivot, cubies, axis3D, axisStr, tangent2D, pxPerRad, startPointer }
/** True while a Tween / scramble animation is playing */
let isAnimating = false;
// Pointer tracking
const pointer = new THREE.Vector2();
const pointerStart = new THREE.Vector2();
// ====================================================================
// UTILITY: Project a 3D point to screen pixel coordinates
// ====================================================================
function toScreen(worldPos, cam, w, h) {
const ndc = worldPos.clone().project(cam);
return new THREE.Vector2(
(ndc.x * 0.5 + 0.5) * w,
(-ndc.y * 0.5 + 0.5) * h,
);
}
/**
* Project a 3D *direction* (not position) anchored at `origin` onto the
* screen and return a normalised 2D vector.
*/
function projectDirToScreen(origin3D, dir3D, cam, w, h) {
const a = toScreen(origin3D, cam, w, h);
const b = toScreen(origin3D.clone().add(dir3D), cam, w, h);
const v = new THREE.Vector2(b.x - a.x, b.y - a.y);
return v.length() < 1e-6 ? v : v.normalize();
}
/**
* Snap a 3D vector to the nearest cardinal axis (±X, ±Y, or ±Z).
* Used to clean up the raycast face normal.
*/
function snapToCardinal(v) {
const ax = Math.abs(v.x), ay = Math.abs(v.y), az = Math.abs(v.z);
const eps = 0.01;
if (ax >= ay && ax >= az) return new THREE.Vector3(v.x > eps ? 1 : (v.x < -eps ? -1 : 0), 0, 0);
if (ay >= ax && ay >= az) return new THREE.Vector3(0, v.y > eps ? 1 : (v.y < -eps ? -1 : 0), 0);
return new THREE.Vector3(0, 0, v.z > eps ? 1 : (v.z < -eps ? -1 : 0));
}
/**
* Given a face normal, return the two candidate rotation axes.
* Example: normal ≈ Z → candidates are X and Y.
*/
function getCandidateAxes(normal) {
const a = Math.abs(normal.x), b = Math.abs(normal.y), c = Math.abs(normal.z);
if (c >= a && c >= b) return ['x', 'y'];
if (b >= a && b >= c) return ['x', 'z'];
return ['y', 'z'];
}
// ====================================================================
// LAYER SELECTION (dynamic — based on world position, not hard-coded)
// ====================================================================
/**
* Return all cubies whose world position along `axis` equals `layerValue`
* (within EPSILON). This is the key to dynamic layer detection.
*/
function getLayerCubies(axis, layerValue) {
const key = ({ x: 'x', y: 'y', z: 'z' })[axis] || axis;
return cubies.filter(c => {
// Use getWorldPosition to handle any parent hierarchy correctly
const wp = new THREE.Vector3();
c.getWorldPosition(wp);
return Math.abs(wp[key] - layerValue) < EPSILON;
});
}
// ====================================================================
// PIVOT-BASED ROTATION ENGINE
// Core idea:
// 1. Create a temporary Object3D "pivot" at world origin.
// 2. pivot.attach(cubie) — magically preserves the cubie's world
// transform while re-parenting it under the pivot.
// 3. Rotate the pivot → all attached cubies orbit around it.
// 4. On completion, scene.attach(cubie) puts cubies back into the
// scene, again preserving the world transform.
// 5. Round positions & rotations to eliminate floating-point drift.
// ====================================================================
/**
* Round a cubie's local position to the nearest integer and its Euler
* rotation to the nearest multiple of 90° (PI/2). This prevents the
* cube from slowly "drifting apart" after many rotations.
*/
function roundCubieTransform(cubie) {
// Position → nearest integer (centres are always at -1, 0, or 1 × STEP)
cubie.position.x = Math.round(cubie.position.x / STEP) * STEP;
cubie.position.y = Math.round(cubie.position.y / STEP) * STEP;
cubie.position.z = Math.round(cubie.position.z / STEP) * STEP;
// Rotation → nearest 90° multiple
const snap = (v) => Math.round(v / (Math.PI / 2)) * (Math.PI / 2);
// Decompose quaternion → Euler for rounding, then write back
const euler = new THREE.Euler().setFromQuaternion(cubie.quaternion, 'XYZ');
cubie.rotation.x = snap(euler.x);
cubie.rotation.y = snap(euler.y);
cubie.rotation.z = snap(euler.z);
}
/**
* Execute one atomic rotation with a Tween animation.
* Used by the snap-to-grid phase and by the scramble sequencer.
*
* @param {string} axisStr 'x' | 'y' | 'z'
* @param {number} layerVal -1 | 0 | 1 (the layer index along the axis)
* @param {number} toAngle target angle in radians (multiple of PI/2)
* @param {number} duration ms
* @returns {Promise<void>}
*/
function executeMove(axisStr, layerVal, toAngle, duration = 180) {
return new Promise(resolve => {
const pivot = new THREE.Object3D();
scene.add(pivot);
const layerCubies = getLayerCubies(axisStr, layerVal);
if (layerCubies.length === 0) {
scene.remove(pivot);
resolve();
return;
}
// Attach — world transform is preserved, cubies now child of pivot
layerCubies.forEach(c => pivot.attach(c));
const start = { v: pivot.rotation[axisStr] };
new TWEEN.Tween(start)
.to({ v: toAngle }, duration)
.easing(TWEEN.Easing.Quadratic.InOut)
.onUpdate(() => { pivot.rotation[axisStr] = start.v; })
.onComplete(() => {
// Detach — cubies return to scene, world transform preserved
layerCubies.forEach(c => scene.attach(c));
layerCubies.forEach(roundCubieTransform);
scene.remove(pivot);
resolve();
})
.start();
});
}
// ====================================================================
// GESTURE RECOGNITION — Projection-based algorithm
//
// Overview:
// 1. On pointer-down, raycast to find the clicked cubie & face normal.
// 2. Snap the face normal to a cardinal direction (±X/±Y/±Z).
// 3. The two *other* cardinal axes are the candidate rotation axes.
// 4. For each candidate axis, compute the "tangent direction" —
// cross(axis, faceNormal) — which is the 3D direction a surface
// point moves when the layer rotates around that axis.
// 5. Project each tangent onto the 2D screen.
// 6. As the user drags, compute the 2D drag vector. Take the dot
// product with each projected tangent; the axis with the highest
// |dot| is the one the user intends to rotate.
// 7. The sign of the dot product gives the rotation direction,
// automatically correcting for back-face / top-face views.
// ====================================================================
function onPointerDown(event) {
if (isAnimating || active !== null) return;
if (event.button !== 0) return; // left-click only
// Normalised device coordinates for raycaster
pointer.x = (event.clientX / window.innerWidth) * 2 - 1;
pointer.y = -(event.clientY / window.innerHeight) * 2 + 1;
pointerStart.set(event.clientX, event.clientY);
raycaster.setFromCamera(pointer, camera);
const hits = raycaster.intersectObjects(cubies, false);
if (hits.length === 0) return; // missed all cubies
const hit = hits[0];
// --- Step 1: World-space face normal ---
// intersect.face.normal is in the geometry's *local* space.
// We transform it by the cubie's world quaternion to get world-space.
const localNormal = hit.face.normal.clone();
const worldQuat = new THREE.Quaternion();
hit.object.getWorldQuaternion(worldQuat);
const worldNormal = localNormal.applyQuaternion(worldQuat).normalize();
const faceNormal = snapToCardinal(worldNormal);
// --- Step 2: Candidate rotation axes (the two NOT equal to faceNormal) ---
const candidates = getCandidateAxes(faceNormal);
// --- Step 3: We haven't chosen an axis yet — we'll do it after the
// first meaningful drag. Store everything we need for the drag phase.
const hitPoint = hit.point.clone();
// Pre-compute tangent projections for each candidate
const screenW = renderer.domElement.clientWidth;
const screenH = renderer.domElement.clientHeight;
const candidateData = candidates.map(axisStr => {
const axis3D = AXIS_VEC[axisStr].clone();
// Tangent direction in 3D: cross(rotationAxis, faceNormal)
const tangent3D = new THREE.Vector3()
.crossVectors(axis3D, faceNormal)
.normalize();
const tangent2D = projectDirToScreen(hitPoint, tangent3D, camera, screenW, screenH);
return { axisStr, axis3D, tangent3D, tangent2D };
});
active = {
hitPoint,
faceNormal,
hitCubie: hit.object, // stored so we don't re-raycast in pointermove
candidates: candidateData,
chosen: null, // determined after first meaningful drag
pivot: null,
cubies: null,
pxPerRad: null,
startPointer: { x: event.clientX, y: event.clientY },
};
// Prevent OrbitControls from picking up this event
event.stopPropagation();
}
function onPointerMove(event) {
if (isAnimating || !active) return;
const dx = event.clientX - active.startPointer.x;
const dy = event.clientY - active.startPointer.y;
const drag2D = new THREE.Vector2(dx, dy);
const DRAG_THRESHOLD = 6; // pixels — minimum drag to commit to an axis
if (drag2D.length() < DRAG_THRESHOLD && !active.chosen) return;
// --- Axis selection (first meaningful drag) ---
if (!active.chosen) {
let bestAxis = null;
let bestDot = -Infinity;
for (const cand of active.candidates) {
// Dot product of drag direction with the projected tangent.
// The tangent whose projection best aligns with the drag wins.
const dot = Math.abs(drag2D.x * cand.tangent2D.x + drag2D.y * cand.tangent2D.y);
if (dot > bestDot) {
bestDot = dot;
bestAxis = cand;
}
}
if (!bestAxis) return;
active.chosen = bestAxis;
// --- Compute pixels-per-radian for 1:1 real-time tracking ---
// We rotate the hit-point by a tiny test angle and measure the
// screen-space displacement.
const center = new THREE.Vector3(0, 0, 0);
const relPoint = active.hitPoint.clone().sub(center);
const testAngle = 0.01; // rad
const rotated = relPoint.clone()
.applyAxisAngle(bestAxis.axis3D, testAngle)
.add(center);
const w = renderer.domElement.clientWidth;
const h = renderer.domElement.clientHeight;
const p1 = toScreen(active.hitPoint, camera, w, h);
const p2 = toScreen(rotated, camera, w, h);
active.pxPerRad = Math.max(p1.distanceTo(p2) / testAngle, 1e-4);
// --- Determine layer value from the clicked cubie's position ---
// We use the cubie reference stored during pointerdown (no re-raycast),
// because cubies may already be moving or the pointer has shifted.
const hitCubie = active.hitCubie;
const wp = new THREE.Vector3();
hitCubie.getWorldPosition(wp);
const layerVal = Math.round(wp[bestAxis.axisStr] / STEP) * STEP;
// --- Create pivot & attach layer cubies ---
const pivot = new THREE.Object3D();
scene.add(pivot);
const layerCubies = getLayerCubies(bestAxis.axisStr, layerVal);
layerCubies.forEach(c => pivot.attach(c));
active.pivot = pivot;
active.cubies = layerCubies;
active.layerVal = layerVal;
active.axisStr = bestAxis.axisStr;
active.tangent2D = bestAxis.tangent2D;
}
// --- Real-time rotation: project total drag onto the tangent ---
const dragAlongTangent = drag2D.x * active.tangent2D.x + drag2D.y * active.tangent2D.y;
const angle = dragAlongTangent / active.pxPerRad; // radians
active.pivot.rotation[active.axisStr] = angle;
}
function onPointerUp(_event) {
if (isAnimating || !active) return;
// If the user never dragged far enough, cancel.
if (!active.chosen || !active.pivot) {
active = null;
return;
}
const currentAngle = active.pivot.rotation[active.axisStr];
// --- Snap to nearest 90° multiple ---
const targetAngle = Math.round(currentAngle / (Math.PI / 2)) * (Math.PI / 2);
isAnimating = true;
document.getElementById('status').classList.add('visible');
const start = { v: currentAngle };
const axisStr = active.axisStr;
const pivot = active.pivot;
const layerCubies = active.cubies;
// Clear active *before* the tween completes so a new drag can't start
// mid-animation (isAnimating flag blocks it).
active = null;
new TWEEN.Tween(start)
.to({ v: targetAngle }, 180)
.easing(TWEEN.Easing.Quadratic.Out)
.onUpdate(() => { pivot.rotation[axisStr] = start.v; })
.onComplete(() => {
// --- Detach cubies from pivot, put back in scene ---
layerCubies.forEach(c => scene.attach(c));
layerCubies.forEach(roundCubieTransform);
scene.remove(pivot);
isAnimating = false;
document.getElementById('status').classList.remove('visible');
})
.start();
}
// ====================================================================
// SCRAMBLE — chain 20 random quarter-turn moves
// ====================================================================
async function scramble(numMoves = 20) {
if (isAnimating || active) return;
isAnimating = true;
document.getElementById('status').classList.add('visible');
const axes = ['x', 'y', 'z'];
const layers = [-STEP, 0, STEP];
const dirs = [1, -1];
// Avoid trivial back-and-forth: track the last move
let last = { axis: '', layer: 0 };
for (let i = 0; i < numMoves; i++) {
let axis, layer, dir;
do {
axis = axes[Math.floor(Math.random() * 3)];
layer = layers[Math.floor(Math.random() * 3)];
dir = dirs[Math.floor(Math.random() * 2)];
} while (
// Skip moves that exactly undo the previous one
axis === last.axis &&
Math.abs(layer - last.layer) < EPSILON &&
i > 0
);
last = { axis, layer };
await executeMove(axis, layer, dir * Math.PI / 2, 100);
}
isAnimating = false;
document.getElementById('status').classList.remove('visible');
}
// ====================================================================
// RESET — rebuild the entire cube from scratch
// ====================================================================
function resetCube() {
if (isAnimating || active) return;
// Remove all existing cubies from the scene.
// NOTE: we do NOT dispose materials — they are shared across all
// cubies and will be reused by the rebuilt meshes.
cubies.forEach(c => {
if (c.parent) c.parent.remove(c);
});
cubies.length = 0;
// Rebuild the 3×3×3 grid at identity rotation
for (let gx = -1; gx <= 1; gx++) {
for (let gy = -1; gy <= 1; gy++) {
for (let gz = -1; gz <= 1; gz++) {
const materials = [
gx === 1 ? matRight : matBlack,
gx === -1 ? matLeft : matBlack,
gy === 1 ? matUp : matBlack,
gy === -1 ? matDown : matBlack,
gz === 1 ? matFront : matBlack,
gz === -1 ? matBack : matBlack,
];
const cubie = new THREE.Mesh(cubieGeo, materials);
cubie.position.set(gx * STEP, gy * STEP, gz * STEP);
cubie.castShadow = true;
cubie.receiveShadow = true;
cubie.userData = { initGrid: { x: gx, y: gy, z: gz } };
scene.add(cubie);
cubies.push(cubie);
}
}
}
}
// ====================================================================
// EVENT BINDING
// ====================================================================
renderer.domElement.addEventListener('pointerdown', onPointerDown);
window.addEventListener('pointermove', onPointerMove);
window.addEventListener('pointerup', onPointerUp);
// If the pointer leaves the window mid-drag, cancel
window.addEventListener('pointerleave', (e) => {
if (active && !isAnimating) {
// Trigger snap by simulating pointerup
onPointerUp(e);
}
});
document.getElementById('scramble').addEventListener('click', () => scramble(20));
document.getElementById('reset').addEventListener('click', resetCube);
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});
// Keyboard shortcuts
window.addEventListener('keydown', (e) => {
if (e.key === 's' || e.key === 'S') scramble(20);
if (e.key === 'r' || e.key === 'R') resetCube();
});
// ====================================================================
// RENDER LOOP
// ====================================================================
function animate(timestamp) {
requestAnimationFrame(animate);
TWEEN.update(timestamp);
controls.update();
renderer.render(scene, camera);
}
requestAnimationFrame(animate);
// ====================================================================
// STARTUP LOG
// ====================================================================
console.log('%c🧊 Rubik\'s Cube Ready %c| %cLeft-drag%c rotate layer %c· %cRight-drag%c orbit %c· %cScroll%c zoom %c· %cS%c scramble %c· %cR%c reset',
'font-size:16px;font-weight:bold;',
'',
'color:#e94560;', '', 'color:#666;',
'color:#4a9eff;', '', 'color:#666;',
'color:#ffd500;', '', 'color:#666;',
'color:#e94560;', '', 'color:#666;',
'color:#4a9eff;', '');
</script>
</body>
</html>

