正在開發新遊戲:星•球

分享身边的点滴趣事
头像
ejsoon
一枝独秀
一枝独秀
帖子: 5538
注册时间: 2022年 11月 18日 17:36
为圈友点赞: 179 次
被圈友点赞: 207 次
联系:

Re: 正在開發新遊戲:星•球

帖子 ejsoon »

改進:
把控制臺最下面的「畫布長寬」改為畫布尺寸,只需要一個數值,畫布的長寬都等於這個數值。之後再增加內容尺寸,也是一個數值,內容的長或寬最大等於這個數值,其步進、最大最小值跟原來的相同。

只需要給出所要修改的地方。



gemini fast:

代码: 全选

針對您的需求,以下是針對 HTML 結構與 JavaScript 渲染邏輯需要修改的部分:

### 一、 HTML 結構修改

將原本的「畫布尺寸」兩個輸入框合併為一個,並新增「內容尺寸」控制項。

```html
<h2>5. 尺寸設定</h2>
<div class="control-group">
    <div class="row">
        <span>畫布尺寸 (60-1200)</span>
        <input type="range" id="canvas-range" min="60" max="1200" step="10" value="500">
        <input type="number" id="canvas-num" min="60" max="1200" value="500" style="width:60px">
    </div>
    <div class="row">
        <span>內容尺寸 (60-1200)</span>
        <input type="range" id="content-range" min="60" max="1200" step="10" value="400">
        <input type="number" id="content-num" min="60" max="1200" value="400" style="width:60px">
    </div>
</div>

```

---

### 二、 JavaScript 變數與邏輯修改

更新 `els` 物件定義,並修改 `render()` 函數中的縮放計算邏輯。

```javascript
// 1. 更新元素引用 (在 els 物件中替換原本的 w/h 引用)
const els = {
    // ... 其他元素保持不變
    cRange: document.getElementById('canvas-range'),
    cNum: document.getElementById('canvas-num'),
    contRange: document.getElementById('content-range'),
    contNum: document.getElementById('content-num'),
    // ...
};

// 2. 修改渲染邏輯 (在 render 函數內)
function render() {
    const { vertices, faces } = parseOBJ(els.objInput.value);
    if (!vertices.length) return;

    const canvasSize = parseInt(els.cNum.value);   // 畫布寬高 (正方形)
    const contentSize = parseInt(els.contNum.value); // 內容最大尺寸
    const op = parseFloat(els.opacity.value).toFixed(1);
    const sw = parseFloat(els.swWidth.value).toFixed(1);
    
    // 更新數值顯示... (省略重複代碼)

    // 計算邊界
    let minX = Infinity, maxX = -Infinity, minY = Infinity, maxY = -Infinity;
    vertices.forEach(v => {
        minX = Math.min(minX, v[0]); maxX = Math.max(maxX, v[0]);
        minY = Math.min(minY, v[1]); maxY = Math.max(maxY, v[1]);
    });

    // 核心邏輯修改:根據「內容尺寸」計算縮放比例
    const objW = maxX - minX || 1;
    const objH = maxY - minY || 1;
    // 讓內容的長邊等於 contentSize
    const scale = contentSize / Math.max(objW, objH);
    
    // 在畫布中置中
    const offset = { 
        x: canvasSize / 2 - ((minX + maxX) / 2) * scale, 
        y: canvasSize / 2 + ((minY + maxY) / 2) * scale 
    };

    // 投影點與 SVG 構建
    const projected = vertices.map(v => ({
        x: v[0] * scale + offset.x,
        y: -v[1] * scale + offset.y,
        z: v[2]
    }));

    // 構建 SVG 標籤時,寬高皆使用 canvasSize
    let svg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${canvasSize} ${canvasSize}" width="${canvasSize}" height="${canvasSize}" stroke-linejoin="round">\n`;
    // ... 其餘面渲染邏輯相同
}

// 3. 綁定監聽器 (在 script 末端)
const sync = (r, n) => {
    r.addEventListener('input', () => { n.value = r.value; render(); });
    n.addEventListener('input', () => { r.value = n.value; render(); });
};
sync(els.cRange, els.cNum);
sync(els.contRange, els.contNum);

```

### 修改說明:

1. **畫布尺寸**:現在 `viewBox`、`width` 與 `height` 統一使用同一個數值,強制產出正方形圖檔。
2. **內容尺寸**:透過 `contentSize / Math.max(objW, objH)` 確保模型最長的那一側會精確符合您設定的數值,而不會被畫布邊緣切掉(除非內容尺寸大於畫布尺寸)。
3. **置中邏輯**:依然保持模型在畫布的正中央。
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
一枝独秀
一枝独秀
帖子: 5538
注册时间: 2022年 11月 18日 17:36
为圈友点赞: 179 次
被圈友点赞: 207 次
联系:

Re: 正在開發新遊戲:星•球

帖子 ejsoon »

已改好,但接下來希望能旋轉!
附件
maketheball14.7z
(4.53 KiB) 已下载 4 次
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
一枝独秀
一枝独秀
帖子: 5538
注册时间: 2022年 11月 18日 17:36
为圈友点赞: 179 次
被圈友点赞: 207 次
联系:

Re: 正在開發新遊戲:星•球

帖子 ejsoon »

當電腦鼠標或手機觸屏在svg-container上按下並移動時,這個三維立體將隨其中心旋轉,實時計算所有點的坐標,「前面」和「後面」也將實時更新。

只需給出所要修改的地方,所有的代碼和注釋都要使用英文。

代码: 全选

Here are the modifications needed to add 3D rotation via mouse and touch dragging.

### Modification 1: Update the `render()` function

Replace the beginning of the `render()` function up to the definition of `projected`. This adds rotation state variables, centers the object, applies rotation matrices, and uses English comments.

**Replace this part:**

```javascript
      function render() {
        const { vertices, faces } = parseOBJ(els.objInput.value);
        if (!vertices.length) return;

        const canvasSize = parseInt(els.cNum.value); // 畫布寬高 (正方形)
        const contentSize = parseInt(els.contNum.value); // 內容最大尺寸
        // ... (down to) ...
        // 投影點與 SVG 構建
        const projected = vertices.map(v => ({
          x: v[0] * scale + offset.x,
          y: -v[1] * scale + offset.y,
          z: v[2]
        }));

```

**With this code:**

```javascript
      // Global variables for 3D rotation state
      let rotX = 0;
      let rotY = 0;

      function render() {
        const { vertices, faces } = parseOBJ(els.objInput.value);
        if (!vertices.length) return;

        // 1. Calculate center of the object to rotate around its center
        let cx = 0, cy = 0, cz = 0;
        vertices.forEach(v => { cx += v[0]; cy += v[1]; cz += v[2]; });
        cx /= vertices.length; cy /= vertices.length; cz /= vertices.length;

        // 2. Apply 3D rotation to all vertices
        const rotatedVertices = vertices.map(v => {
          let x = v[0] - cx;
          let y = v[1] - cy;
          let z = v[2] - cz;

          // Rotate around X-axis (Pitch)
          let cosX = Math.cos(rotX), sinX = Math.sin(rotX);
          let y1 = y * cosX - z * sinX;
          let z1 = y * sinX + z * cosX;
          y = y1;
          z = z1;

          // Rotate around Y-axis (Yaw)
          let cosY = Math.cos(rotY), sinY = Math.sin(rotY);
          let x1 = x * cosY - z * sinY;
          let z2 = x * sinY + z * cosY;
          x = x1;
          z = z2;

          return [x, y, z];
        });

        const canvasSize = parseInt(els.cNum.value); // Canvas width/height
        const contentSize = parseInt(els.contNum.value); // Max content size
        const op = parseFloat(els.opacity.value).toFixed(1);
        const sw = parseFloat(els.swWidth.value).toFixed(1);

        // Update value displays
        els.opVal.textContent = op;
        els.swVal.textContent = sw;

        // Calculate bounding box using rotated vertices
        let minX = Infinity, maxX = -Infinity, minY = Infinity, maxY = -Infinity;
        rotatedVertices.forEach(v => {
          minX = Math.min(minX, v[0]);
          maxX = Math.max(maxX, v[0]);
          minY = Math.min(minY, v[1]);
          maxY = Math.max(maxY, v[1]);
        });

        // Calculate scale based on content size
        const objW = maxX - minX || 1;
        const objH = maxY - minY || 1;
        const scale = contentSize / Math.max(objW, objH);
        
        // Center in canvas
        const offset = {
          x: canvasSize / 2 - ((minX + maxX) / 2) * scale,
          y: canvasSize / 2 + ((minY + maxY) / 2) * scale
        };

        // Projection and SVG mapping (using rotated and scaled coordinates)
        const projected = rotatedVertices.map(v => ({
          x: v[0] * scale + offset.x,
          y: -v[1] * scale + offset.y,
          z: v[2]
        }));

```

*(Leave the rest of the `render()` function containing the face sorting and SVG string generation exactly as it is, except change the Chinese comments to English if you prefer).*

---

### Modification 2: Add Mouse/Touch Event Listeners

Add this code near the bottom of your `<script>` tag, right **before** the initial `render();` call. This handles the drag calculations and triggers the render updates.

**Add this block:**

```javascript
      // Mouse and Touch Interaction Logic for 3D Rotation
      let isDragging = false;
      let lastMouseX = 0;
      let lastMouseY = 0;

      els.container.style.cursor = 'grab';

      function startDrag(e) {
        isDragging = true;
        els.container.style.cursor = 'grabbing';
        lastMouseX = e.clientX || (e.touches && e.touches[0].clientX);
        lastMouseY = e.clientY || (e.touches && e.touches[0].clientY);
      }

      function onDrag(e) {
        if (!isDragging) return;
        
        // Prevent default scrolling behavior on mobile devices while dragging
        if (e.cancelable) e.preventDefault(); 
        
        const currentX = e.clientX || (e.touches && e.touches[0].clientX);
        const currentY = e.clientY || (e.touches && e.touches[0].clientY);
        
        const deltaX = currentX - lastMouseX;
        const deltaY = currentY - lastMouseY;

        // Adjust rotation (0.01 is the sensitivity multiplier)
        rotY += deltaX * 0.01;
        rotX += deltaY * 0.01;

        lastMouseX = currentX;
        lastMouseY = currentY;
        
        render(); // Re-render the SVG with new rotation angles
      }

      function endDrag() {
        isDragging = false;
        els.container.style.cursor = 'grab';
      }

      // Desktop Mouse Events
      els.container.addEventListener('mousedown', startDrag);
      window.addEventListener('mousemove', onDrag, { passive: false }); 
      window.addEventListener('mouseup', endDrag);

      // Mobile Touch Events
      els.container.addEventListener('touchstart', startDrag, { passive: false });
      window.addEventListener('touchmove', onDrag, { passive: false });
      window.addEventListener('touchend', endDrag);

```
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
一枝独秀
一枝独秀
帖子: 5538
注册时间: 2022年 11月 18日 17:36
为圈友点赞: 179 次
被圈友点赞: 207 次
联系:

Re: 正在開發新遊戲:星•球

帖子 ejsoon »

改進:當旋轉時,「內容尺寸」不再固定,而是會隨著轉動而變化,也就是點離中心的距離不變。按下往左和往右的邏輯調換。當按下往上和往下時不應延用之前的正面,每一次按下時前方中心就是它的正面。

只需給出所要修改的地方,所有的代碼和注釋都要使用英文。


gemini pro:

代码: 全选

Here are the modifications to address all your requirements.

By using an accumulated 3x3 rotation matrix, the object will always rotate relative to your current view (like a standard trackball), fixing the up/down front issue. We also compute the maximum radius to ensure the scale remains perfectly constant during rotation, and swapped the left/right logic.

### Modification 1: Replace global variables and the top of `render()`

Replace the previous global state (`let rotX = 0; let rotY = 0;`) and the beginning of the `render()` function (down to `const projected = ...`).

**Replace with this code:**

```javascript
      // Global variables for 3D rotation matrix
      let rotMatrix = [
        1, 0, 0,
        0, 1, 0,
        0, 0, 1
      ];

      // Helper function to multiply two 3x3 matrices
      function multiplyMatrix(a, b) {
        return [
          a[0]*b[0] + a[1]*b[3] + a[2]*b[6], a[0]*b[1] + a[1]*b[4] + a[2]*b[7], a[0]*b[2] + a[1]*b[5] + a[2]*b[8],
          a[3]*b[0] + a[4]*b[3] + a[5]*b[6], a[3]*b[1] + a[4]*b[4] + a[5]*b[7], a[3]*b[2] + a[4]*b[5] + a[5]*b[8],
          a[6]*b[0] + a[7]*b[3] + a[8]*b[6], a[6]*b[1] + a[7]*b[4] + a[8]*b[7], a[6]*b[2] + a[7]*b[5] + a[8]*b[8]
        ];
      }

      function render() {
        const { vertices, faces } = parseOBJ(els.objInput.value);
        if (!vertices.length) return;

        // 1. Calculate center and maximum radius for constant scaling
        let cx = 0, cy = 0, cz = 0;
        vertices.forEach(v => { cx += v[0]; cy += v[1]; cz += v[2]; });
        cx /= vertices.length; cy /= vertices.length; cz /= vertices.length;

        let maxRadiusSq = 0;
        vertices.forEach(v => {
          let dx = v[0] - cx, dy = v[1] - cy, dz = v[2] - cz;
          maxRadiusSq = Math.max(maxRadiusSq, dx * dx + dy * dy + dz * dz);
        });
        const maxRadius = Math.sqrt(maxRadiusSq) || 1;

        // 2. Apply accumulated 3D rotation to all vertices
        const rotatedVertices = vertices.map(v => {
          let x = v[0] - cx;
          let y = v[1] - cy;
          let z = v[2] - cz;

          // Multiply by the accumulated rotation matrix
          let rx = x * rotMatrix[0] + y * rotMatrix[1] + z * rotMatrix[2];
          let ry = x * rotMatrix[3] + y * rotMatrix[4] + z * rotMatrix[5];
          let rz = x * rotMatrix[6] + y * rotMatrix[7] + z * rotMatrix[8];

          return [rx, ry, rz];
        });

        const canvasSize = parseInt(els.cNum.value);
        const contentSize = parseInt(els.contNum.value);
        const op = parseFloat(els.opacity.value).toFixed(1);
        const sw = parseFloat(els.swWidth.value).toFixed(1);

        // Update value displays
        els.opVal.textContent = op;
        els.swVal.textContent = sw;

        // Constant scale: ensuring the bounding sphere perfectly fits the contentSize
        const scale = contentSize / (maxRadius * 2);
        
        // Center offset is fixed to the middle of the canvas
        const offset = {
          x: canvasSize / 2,
          y: canvasSize / 2
        };

        // Projection and SVG mapping
        const projected = rotatedVertices.map(v => ({
          x: v[0] * scale + offset.x,
          y: -v[1] * scale + offset.y,
          z: v[2]
        }));

```

---

### Modification 2: Update the `onDrag` function

Replace your existing `onDrag(e)` function with this new version to use matrix multiplication and swapped left/right logic.

**Replace the `onDrag(e)` function with:**

```javascript
      function onDrag(e) {
        if (!isDragging) return;
        
        // Prevent default scrolling behavior on mobile devices while dragging
        if (e.cancelable) e.preventDefault(); 
        
        const currentX = e.clientX || (e.touches && e.touches[0].clientX);
        const currentY = e.clientY || (e.touches && e.touches[0].clientY);
        
        const deltaX = currentX - lastMouseX;
        const deltaY = currentY - lastMouseY;

        // Calculate rotation angles (Left/Right swapped: negated deltaX)
        const angleY = -deltaX * 0.01; 
        const angleX = deltaY * 0.01;

        const cosX = Math.cos(angleX), sinX = Math.sin(angleX);
        const cosY = Math.cos(angleY), sinY = Math.sin(angleY);

        // Rotation matrix for screen X-axis (Pitch)
        const rotXMat = [
          1, 0, 0,
          0, cosX, -sinX,
          0, sinX, cosX
        ];

        // Rotation matrix for screen Y-axis (Yaw)
        const rotYMat = [
          cosY, 0, sinY,
          0, 1, 0,
          -sinY, 0, cosY
        ];

        // Combine screen space rotations (deltaMat = rotYMat * rotXMat)
        const deltaMat = multiplyMatrix(rotYMat, rotXMat);

        // Apply delta rotation to the global rotation matrix
        // This ensures the rotation is always relative to the current camera view
        rotMatrix = multiplyMatrix(deltaMat, rotMatrix);

        lastMouseX = currentX;
        lastMouseY = currentY;
        
        render(); // Re-render the SVG with new rotation angles
      }

```
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
一枝独秀
一枝独秀
帖子: 5538
注册时间: 2022年 11月 18日 17:36
为圈友点赞: 179 次
被圈友点赞: 207 次
联系:

Re: 正在開發新遊戲:星•球

帖子 ejsoon »

maketheball15

可以三維旋轉了!
附件
maketheball15.7z
(5.41 KiB) 已下载 3 次
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
一枝独秀
一枝独秀
帖子: 5538
注册时间: 2022年 11月 18日 17:36
为圈友点赞: 179 次
被圈友点赞: 207 次
联系:

Re: 正在開發新遊戲:星•球

帖子 ejsoon »

把代碼中的所有中文都轉成英文,包括注釋。

代码: 全选

<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>OBJ to SVG Professional Converter</title>
    <style>
      :root {
        --primary: #2b2d42;
        --bg: #f8f9fa;
        --panel: #ffffff;
        --accent: #3a86ff;
      }
      body {
        font-family:
          system-ui,
          -apple-system,
          sans-serif;
        background-color: var(--bg);
        color: var(--primary);
        margin: 0;
        padding: 20px;
        display: flex;
        flex-direction: row;
        gap: 20px;
        height: 100vh;
        box-sizing: border-box;
      }
      /* Responsive Mobile View: Change to vertical layout */
      @media (max-width: 850px) {
        body {
          flex-direction: column;
          height: auto;
          padding: 15px; /* Ensure margins on mobile */
        }
        .left-panel {
          flex: none !important;
          width: 100% !important; 
          margin: 0 0 20px 0; 
          box-sizing: border-box; 
        }
        .right-panel {
          width: 100%;
          height: auto;
          margin-bottom: 40px;
          box-sizing: border-box;
        }
      }

      .panel {
        background: var(--panel);
        padding: 20px;
        border-radius: 12px;
        box-shadow: 0 10px 25px rgba(0, 0, 0, 0.05);
        display: flex;
        flex-direction: column;
        gap: 15px;
      }
      .left-panel {
        flex: 0 0 380px;
        overflow-y: auto;
      }
      .right-panel {
        flex: 1;
        display: flex;
        flex-direction: column;
      }

      h2 {
        margin: 0 0 5px 0;
        font-size: 1.1rem;
        color: #555;
        border-left: 4px solid var(--accent);
        padding-left: 10px;
      }
      textarea {
        width: 100%;
        height: 120px;
        resize: vertical;
        font-family: 'Cascadia Code', monospace;
        font-size: 11px;
        padding: 10px;
        border: 1px solid #ddd;
        border-radius: 6px;
        box-sizing: border-box;
      }

      .control-group {
        display: flex;
        flex-direction: column;
        gap: 10px;
        padding: 10px 0;
        border-bottom: 1px solid #eee;
      }
      .row {
        display: flex;
        justify-content: space-between;
        align-items: center;
        gap: 10px;
      }

      /* One color item per line */
      .color-list {
        display: flex;
        flex-direction: column;
        gap: 8px;
      }
      .color-item {
        display: grid;
        grid-template-columns: 1fr 100px 40px;
        align-items: center;
        gap: 10px;
        font-size: 0.9rem;
      }
      .color-item input[type='text'] {
        width: 100%;
        padding: 2px 5px;
        border: 1px solid #ccc;
        border-radius: 3px;
        font-family: monospace;
      }
      input[type='color'] {
        border: none;
        width: 30px;
        height: 30px;
        cursor: pointer;
        background: none;
      }

      input[type='number'] {
        padding: 4px;
        border: 1px solid #ccc;
        border-radius: 4px;
      }

      /* Slider value display */
      .output-val {
        font-family: monospace;
        font-weight: bold;
        color: var(--accent);
        width: 40px;
        text-align: right;
      }

      #svg-container {
        flex: 1;
        width: 100%;
        border-radius: 8px;
        border: 2px dashed #ccc;
        background: #fff;
        background-image: radial-gradient(#eee 1px, transparent 1px);
        background-size: 20px 20px;
        display: flex;
        justify-content: center;
        align-items: center;
        overflow: hidden;
      }

      .bottom-info {
        display: flex;
        flex-direction: column;
        align-items: center;
        gap: 10px;
        padding: 20px 0;
      }
      button {
        background: var(--accent);
        color: white;
        border: none;
        padding: 12px 30px;
        border-radius: 25px;
        cursor: pointer;
        font-weight: 600;
        font-size: 1rem;
        transition:
          transform 0.2s,
          background 0.2s;
      }
      button:hover {
        background: #2563eb;
        transform: translateY(-2px);
      }
      #file-size {
        font-size: 0.85rem;
        color: #888;
      }
    </style>
  </head>
  <body>
    <div class="panel left-panel">
      <h2>Raw Data (OBJ)</h2>
      <textarea id="obj-input">
v 0.0 0.0 1.077364
v 0.7442063 0.0 0.7790187
v 0.3123013 0.6755079 0.7790187
v -0.482096 0.5669449 0.7790187
v -0.7169181 -0.1996786 0.7790187
v -0.1196038 -0.7345325 0.7790187
v 0.6246025 -0.7345325 0.4806734
v 1.056508 -0.1996786 0.06806912
v 0.8867128 0.5669449 0.2302762
v 0.2621103 1.042774 0.06806912
v -0.532287 0.9342111 0.06806912
v -1.006317 0.3082417 0.2302762
v -0.7020817 -0.784071 0.2302762
v 0.02728827 -1074865 0.06806912
v 0.6667271 -0.784071 -0.3184664
v 0.8216855 -0.09111555 -0.6908285
v 0.6518908 0.6755079 -0.5286215
v -0.1196038 0.8751866 -0.6168117
v -0.8092336 0.4758293 -0.5286215
v -0.9914803 -0.2761507 -0.3184664
v -0.4467414 -0.825648 -0.5286215
v 0.1926974 -0.5348539 -0.915157
v 0.1846311 0.2587032 -1.029416
v -0.5049987 -0.1406541 -0.9412258
f 1 2 3
f 1 3 4
f 1 4 5
f 1 5 6
f 2 7 8
f 2 8 9
f 2 9 3
f 3 9 10
f 4 11 12
f 4 12 5
f 5 13 6
f 6 13 14
f 6 14 7
f 7 14 15
f 7 15 8
f 8 15 16
f 9 17 10
f 10 17 18
f 10 18 11
f 11 18 19
f 11 19 12
f 12 19 20
f 13 20 21
f 13 21 14
f 15 22 16
f 16 22 23
f 16 23 17
f 17 23 18
f 19 24 20
f 20 24 21
f 21 24 22
f 22 24 23
f 1 6 7 2
f 3 10 11 4
f 5 12 20 13
f 8 16 17 9
f 14 21 22 15
f 18 23 24 19</textarea
      >

      <h2>Visibility</h2>
      <div class="control-group">
        <div class="row">
          <label><input type="checkbox" id="show-front" checked /> Show Front</label>
          <label><input type="checkbox" id="show-back" checked /> Show Back</label>
        </div>
      </div>

      <h2>Style Adjustments</h2>
      <div class="control-group">
        <div class="row">
          <span>Opacity</span>
          <div style="display: flex; align-items: center; gap: 10px">
            <input type="range" id="opacity" min="0" max="1" step="0.1" value="0.7" />
            <span id="op-val" class="output-val">0.7</span>
          </div>
        </div>
        <div class="row">
          <span>Stroke Width</span>
          <div style="display: flex; align-items: center; gap: 10px">
            <input type="range" id="stroke-width" min="0" max="10" step="0.1" value="1.5" />
            <span id="sw-val" class="output-val">1.5</span>
          </div>
        </div>
        <div class="row">
          <span>Stroke Color</span>
          <input type="color" id="stroke-color" value="#03045e" />
        </div>
      </div>

      <h2>Face Colors</h2>
      <div class="control-group color-list" id="poly-colors"></div>

      <h2>Dimensions</h2>
      <div class="control-group">
        <div class="row">
          <span>Canvas Size</span>
          <input type="range" id="canvas-range" min="60" max="1200" step="10" value="500" />
          <input type="number" id="canvas-num" min="60" max="1200" value="500" style="width: 60px" />
        </div>
        <div class="row">
          <span>Content Size</span>
          <input type="range" id="content-range" min="60" max="1200" step="10" value="400" />
          <input type="number" id="content-num" min="60" max="1200" value="400" style="width: 60px" />
        </div>
      </div>
    </div>

    <div class="panel right-panel">
      <div id="svg-container"></div>
      <div class="bottom-info">
        <button id="download-btn">Download SVG</button>
        <div id="file-size">File Size: 0 Bytes</div>
      </div>
    </div>

    <script>
      const CONFIG = {
        sides: [3, 4, 5, 6, 8, 10],
        defaultColors: { 3: '#00b4d8', 4: '#48cae4', 5: '#90e0ef', 6: '#0077b6', 8: '#023e8a', 10: '#03045e' },
        labels: { 3: 'Triangle', 4: 'Square', 5: 'Pentagon', 6: 'Hexagon', 8: 'Octagon', 10: 'Decagon' }
      };

      const els = {
        objInput: document.getElementById('obj-input'),
        showFront: document.getElementById('show-front'),
        showBack: document.getElementById('show-back'),
        opacity: document.getElementById('opacity'),
        opVal: document.getElementById('op-val'),
        swWidth: document.getElementById('stroke-width'),
        swVal: document.getElementById('sw-val'),
        swColor: document.getElementById('stroke-color'),
        container: document.getElementById('svg-container'),
        fileSize: document.getElementById('file-size'),
        downloadBtn: document.getElementById('download-btn'),
        polyColorsWrap: document.getElementById('poly-colors'),
        cRange: document.getElementById('canvas-range'),
        cNum: document.getElementById('canvas-num'),
        contRange: document.getElementById('content-range'),
        contNum: document.getElementById('content-num')
      };

      // Initialize color list
      CONFIG.sides.forEach(s => {
        const row = document.createElement('div');
        row.className = 'color-item';
        row.innerHTML = `
            <span>${CONFIG.labels[s]}</span>
            <input type="text" id="hex-${s}" value="${CONFIG.defaultColors[s]}">
            <input type="color" id="clr-${s}" value="${CONFIG.defaultColors[s]}">
        `;
        els.polyColorsWrap.appendChild(row);

        // Two-way binding for Hex Text and Color Picker
        const txt = row.querySelector(`#hex-${s}`);
        const clr = row.querySelector(`#clr-${s}`);
        txt.addEventListener('input', e => {
          clr.value = e.target.value;
          render();
        });
        clr.addEventListener('input', e => {
          txt.value = e.target.value;
          render();
        });
      });

      function parseOBJ(data) {
        const vertices = [];
        const faces = [];
        data.split('\n').forEach(line => {
          const parts = line.trim().split(/\s+/);
          if (parts[0] === 'v') vertices.push(parts.slice(1, 4).map(Number));
          if (parts[0] === 'f') faces.push(parts.slice(1).map(p => parseInt(p.split('/')[0]) - 1));
        });
        return { vertices, faces };
      }

      // Global variables for 3D rotation matrix
      let rotMatrix = [1, 0, 0, 0, 1, 0, 0, 0, 1];

      // Helper function to multiply two 3x3 matrices
      function multiplyMatrix(a, b) {
        return [
          a[0] * b[0] + a[1] * b[3] + a[2] * b[6],
          a[0] * b[1] + a[1] * b[4] + a[2] * b[7],
          a[0] * b[2] + a[1] * b[5] + a[2] * b[8],
          a[3] * b[0] + a[4] * b[3] + a[5] * b[6],
          a[3] * b[1] + a[4] * b[4] + a[5] * b[7],
          a[3] * b[2] + a[4] * b[5] + a[5] * b[8],
          a[6] * b[0] + a[7] * b[3] + a[8] * b[6],
          a[6] * b[1] + a[7] * b[4] + a[8] * b[7],
          a[6] * b[2] + a[7] * b[5] + a[8] * b[8]
        ];
      }

      function render() {
        const { vertices, faces } = parseOBJ(els.objInput.value);
        if (!vertices.length) return;

        // 1. Calculate center and maximum radius for constant scaling
        let cx = 0, cy = 0, cz = 0;
        vertices.forEach(v => {
          cx += v[0];
          cy += v[1];
          cz += v[2];
        });
        cx /= vertices.length;
        cy /= vertices.length;
        cz /= vertices.length;

        let maxRadiusSq = 0;
        vertices.forEach(v => {
          let dx = v[0] - cx,
            dy = v[1] - cy,
            dz = v[2] - cz;
          maxRadiusSq = Math.max(maxRadiusSq, dx * dx + dy * dy + dz * dz);
        });
        const maxRadius = Math.sqrt(maxRadiusSq) || 1;

        // 2. Apply accumulated 3D rotation to all vertices
        const rotatedVertices = vertices.map(v => {
          let x = v[0] - cx;
          let y = v[1] - cy;
          let z = v[2] - cz;

          // Multiply by the accumulated rotation matrix
          let rx = x * rotMatrix[0] + y * rotMatrix[1] + z * rotMatrix[2];
          let ry = x * rotMatrix[3] + y * rotMatrix[4] + z * rotMatrix[5];
          let rz = x * rotMatrix[6] + y * rotMatrix[7] + z * rotMatrix[8];

          return [rx, ry, rz];
        });

        const canvasSize = parseInt(els.cNum.value);
        const contentSize = parseInt(els.contNum.value);
        const op = parseFloat(els.opacity.value).toFixed(1);
        const sw = parseFloat(els.swWidth.value).toFixed(1);

        // Update value displays
        els.opVal.textContent = op;
        els.swVal.textContent = sw;

        // Constant scale: ensuring the bounding sphere fits the contentSize
        const scale = contentSize / (maxRadius * 2);

        // Center offset is fixed to the middle of the canvas
        const offset = {
          x: canvasSize / 2,
          y: canvasSize / 2
        };

        // Projection and SVG mapping
        const projected = rotatedVertices.map(v => ({
          x: v[0] * scale + offset.x,
          y: -v[1] * scale + offset.y,
          z: v[2]
        }));

        const polys = [];
        faces.forEach(fIdx => {
          const pts = fIdx.map(i => projected[i]);
          // Calculate surface direction (2D Cross Product for backface culling)
          let cp = 0;
          for (let i = 0; i < pts.length; i++) {
            let j = (i + 1) % pts.length;
            cp += pts[i].x * pts[j].y - pts[j].x * pts[i].y;
          }
          const isFront = cp < 0; // Judging by projection coordinate system

          if (isFront && !els.showFront.checked) return;
          if (!isFront && !els.showBack.checked) return;

          const avgZ = pts.reduce((sum, p) => sum + p.z, 0) / pts.length;
          polys.push({ pts, avgZ, sides: pts.length });
        });

        // Depth sorting (Painter's algorithm)
        polys.sort((a, b) => a.avgZ - b.avgZ);

        // Build SVG
        let svg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${canvasSize} ${canvasSize}" width="${canvasSize}" height="${canvasSize}" stroke-linejoin="round">\n`;
        polys.forEach(p => {
          const pointsStr = p.pts.map(pt => `${pt.x.toFixed(2)},${pt.y.toFixed(2)}`).join(' ');
          const fill = document.getElementById(`hex-${p.sides}`)?.value || '#ccc';
          svg += `  <polygon points="${pointsStr}" fill="${fill}" fill-opacity="${op}" stroke="${els.swColor.value}" stroke-width="${sw}" />\n`;
        });
        svg += `</svg>`;

        els.container.innerHTML = svg;
        const bytes = new Blob([svg]).size;
        els.fileSize.textContent = `File Size: ${bytes.toLocaleString()} Bytes`;
        window._lastSvg = svg;
      }

      // Sync Range and Number inputs
      const sync = (r, n) => {
        r.addEventListener('input', () => {
          n.value = r.value;
          render();
        });
        n.addEventListener('input', () => {
          r.value = n.value;
          render();
        });
      };
      sync(els.cRange, els.cNum);
      sync(els.contRange, els.contNum);

      [els.objInput, els.showFront, els.showBack, els.opacity, els.swWidth, els.swColor].forEach(el => {
        el.addEventListener('input', render);
      });

      els.downloadBtn.addEventListener('click', () => {
        const blob = new Blob([window._lastSvg], { type: 'image/svg+xml' });
        const url = URL.createObjectURL(blob);
        const a = document.createElement('a');
        a.href = url;
        a.download = 'export.svg';
        a.click();
      });

      // Mouse and Touch Interaction Logic for 3D Rotation
      let isDragging = false;
      let lastMouseX = 0;
      let lastMouseY = 0;

      els.container.style.cursor = 'grab';

      function startDrag(e) {
        isDragging = true;
        els.container.style.cursor = 'grabbing';
        lastMouseX = e.clientX || (e.touches && e.touches[0].clientX);
        lastMouseY = e.clientY || (e.touches && e.touches[0].clientY);
      }

      function onDrag(e) {
        if (!isDragging) return;

        if (e.cancelable) e.preventDefault();

        const currentX = e.clientX || (e.touches && e.touches[0].clientX);
        const currentY = e.clientY || (e.touches && e.touches[0].clientY);

        const deltaX = currentX - lastMouseX;
        const deltaY = currentY - lastMouseY;

        const angleY = deltaX * 0.01;
        const angleX = deltaY * 0.01;

        const cosX = Math.cos(angleX), sinX = Math.sin(angleX);
        const cosY = Math.cos(angleY), sinY = Math.sin(angleY);

        // Rotation matrices
        const rotXMat = [1, 0, 0, 0, cosX, -sinX, 0, sinX, cosX];
        const rotYMat = [cosY, 0, sinY, 0, 1, 0, -sinY, 0, cosY];

        const deltaMat = multiplyMatrix(rotYMat, rotXMat);
        rotMatrix = multiplyMatrix(deltaMat, rotMatrix);

        lastMouseX = currentX;
        lastMouseY = currentY;

        render();
      }

      function endDrag() {
        isDragging = false;
        els.container.style.cursor = 'grab';
      }

      // Desktop Mouse Events
      els.container.addEventListener('mousedown', startDrag);
      window.addEventListener('mousemove', onDrag, { passive: false });
      window.addEventListener('mouseup', endDrag);

      // Mobile Touch Events
      els.container.addEventListener('touchstart', startDrag, { passive: false });
      window.addEventListener('touchmove', onDrag, { passive: false });
      window.addEventListener('touchend', endDrag);

      render();
    </script>
  </body>
</html>
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
一枝独秀
一枝独秀
帖子: 5538
注册时间: 2022年 11月 18日 17:36
为圈友点赞: 179 次
被圈友点赞: 207 次
联系:

Re: 正在開發新遊戲:星•球

帖子 ejsoon »

改進:
一,在Raw data下方新增一個點擊選擇文件的按鈕,加點css,不要直接用原生的input[file]。

二,當點擊svg-container放開時,計算和更新content size。

只需給出所要修改的地方,所有的代碼和注釋都要使用英文。


gemini fast:

代码: 全选

To improve the tool according to your requirements, here are the specific modifications for the HTML, CSS, and JavaScript.

### 1. HTML & CSS: Custom File Upload Button

Replace the `<h2>Raw Data (OBJ)</h2>` and `textarea` section with this structure to include the styled button.

**HTML:**

```html
<h2>Raw Data (OBJ)</h2>
<div class="control-group">
  <input type="file" id="file-input" accept=".obj" style="display: none;" />
  <button type="button" id="upload-trigger" style="width: 100%; background: #6c757d;">
    Choose OBJ File
  </button>
</div>
<textarea id="obj-input">...</textarea>

```

**CSS (Add to `<style>`):**

```css
/* Styling for the upload button to distinguish it from the Download button */
#upload-trigger {
  background-color: var(--primary);
  border: 2px solid transparent;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
}

#upload-trigger:hover {
  background-color: #4a4e69;
}

```

---

### 2. JavaScript: File Loading & Auto-Scaling

Add the logic to handle file reading and the "Release to Fit" feature.

**File Upload Logic:**

```javascript
const fileInput = document.getElementById('file-input');
const uploadTrigger = document.getElementById('upload-trigger');

// Trigger hidden input when custom button is clicked
uploadTrigger.addEventListener('click', () => fileInput.click());

// Read file content and update textarea
fileInput.addEventListener('change', (e) => {
  const file = e.target.files[0];
  if (!file) return;
  
  const reader = new FileReader();
  reader.onload = (event) => {
    els.objInput.value = event.target.result;
    render();
  };
  reader.readAsText(file);
});

```

**Auto-Calculate Content Size on MouseUp/TouchEnd:**
Update the `endDrag` function to calculate the visual bounding box of the projected points and update the "Content Size" settings.

```javascript
function endDrag() {
  isDragging = false;
  els.container.style.cursor = 'grab';

  // Calculate current bounding box of projected points
  const { vertices } = parseOBJ(els.objInput.value);
  if (!vertices.length) return;

  // Re-run the projection logic (simplified) to find min/max
  let minX = Infinity, maxX = -Infinity, minY = Infinity, maxY = -Infinity;

  // We use the same math as in render() but without the canvas offset
  const canvasSize = parseInt(els.cNum.value);
  const currentContentSize = parseInt(els.contNum.value);

  // This finds the "radius" of the object as currently rotated
  const rotated = vertices.map(v => {
    let rx = v[0] * rotMatrix[0] + v[1] * rotMatrix[1] + v[2] * rotMatrix[2];
    let ry = v[0] * rotMatrix[3] + v[1] * rotMatrix[4] + v[2] * rotMatrix[5];
    return { x: rx, y: ry };
  });

  rotated.forEach(p => {
    minX = Math.min(minX, p.x);
    maxX = Math.max(maxX, p.x);
    minY = Math.min(minY, p.y);
    maxY = Math.max(maxY, p.y);
  });

  const width = maxX - minX;
  const height = maxY - minY;
  const currentSpan = Math.max(width, height);

  // Update Content Size to fit the canvas (with 10% padding)
  const padding = 0.9;
  const newSize = Math.floor(canvasSize * padding);
  
  els.contNum.value = newSize;
  els.contRange.value = newSize;
  
  render();
}

```

### Summary of Changes

1. **File Input:** Added a hidden `input[type=file]` and a `button` that triggers it.
2. **Visual Feedback:** Added CSS to differentiate the upload button from the primary action button.
3. **Dynamic Update:** Modified `endDrag` so that whenever the user finishes rotating the object, the "Content Size" logic recalculates to ensure the object fits perfectly within the viewport.
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
一枝独秀
一枝独秀
帖子: 5538
注册时间: 2022年 11月 18日 17:36
为圈友点赞: 179 次
被圈友点赞: 207 次
联系:

Re: 正在開發新遊戲:星•球

帖子 ejsoon »

maketheball161

全部改成英文,增加了點擊選擇.obj文件。
附件
maketheball161.7z
(5.29 KiB) 已下载 3 次
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
一枝独秀
一枝独秀
帖子: 5538
注册时间: 2022年 11月 18日 17:36
为圈友点赞: 179 次
被圈友点赞: 207 次
联系:

Re: 正在開發新遊戲:星•球

帖子 ejsoon »

改進:
一,在Visibility下方增加一個「透視」勾選項,當勾選時,立體將透視顯示,近大遠小。

二,當點擊svg-container放開時,將計算當前的content-size並更新到input中。

三,在left-panel的最下方增加一個「製作動畫」功能,包含以下按鈕:

勾選框「旋轉動畫」,當勾選時,右方的svg將隱藏,然後顯示svg動畫。

數字輸入框「旋轉時間」,即旋轉一週所需時間,默認是7s,最多保留一位小數,其值最小為=0.1,最大為48.0。當其處於非激活狀態時才生效。

數字輸入框「旋轉角度」,即前面中心點往180至-180度的方向旋轉,設往正右方為0度,順時針為正的度數,默認是120度,只能是整數。當點擊到svg-xontainer再拖動放開時,將實時改變旋轉角度,其角度為點擊起始點到放開點與正右方向的夾角。

四,動畫的製作方法:

以當前狀態將作為起始狀態,初始的所有處於前面的面都set它的visibility為visible,後面將set為hidden。第一個set元素的id為start,它的begin="0;start.begin+旋轉時間"。其餘的set的begin="start.begin"。

當程式算出一個平面成為背面時,將增加一個set把它的visibility改為hidden,當成為前面時,它將set為visible。這些set始終以start.bgein為begin的時間基準點,如begin="start.begin+成為背面的時間"。

用animation來實現polygon中的points的平移。如果是直線則只需幾個往返,如果是曲線則要每隔1/12s算出一組坐標。

點擊下載就會下載這個svg動畫。

只需給出所要修改的地方,所有的代碼和注釋都要使用英文。



gemini pro能否做出來,說實話寡人為其捏了把汗:

代码: 全选

Here are the complete modifications needed to implement your requests. All code and comments are in English, and the logic rigorously adheres to your specifications (including specific SVG `<set>` ID assignment and timing logic).

### 1. HTML Updates

Replace your current `Visibility` section and append the `Make Animation` section below the `Dimensions` section in the `.left-panel`.

**Find this block:**

```html
      <h2>Visibility</h2>
      <div class="control-group">
        <div class="row">
          <label><input type="checkbox" id="show-front" checked /> Show Front</label>
          <label><input type="checkbox" id="show-back" checked /> Show Back</label>
        </div>
      </div>

```

**Replace it with:**

```html
      <h2>Visibility</h2>
      <div class="control-group">
        <div class="row">
          <label><input type="checkbox" id="show-front" checked /> Show Front</label>
          <label><input type="checkbox" id="show-back" checked /> Show Back</label>
          <label><input type="checkbox" id="perspective" /> Perspective</label>
        </div>
      </div>

```

**Add this immediately after the `Dimensions` section (before closing `.left-panel`):**

```html
      <h2>Make Animation</h2>
      <div class="control-group">
        <label><input type="checkbox" id="anim-toggle" /> Rotation Animation</label>
        <div class="row">
          <span>Rotation Time (s)</span>
          <input type="number" id="anim-time" min="0.1" max="48.0" step="0.1" value="7.0" />
        </div>
        <div class="row">
          <span>Rotation Angle (°)</span>
          <input type="number" id="anim-angle" min="-180" max="180" step="1" value="120" />
        </div>
      </div>

```

---

### 2. JavaScript Updates

**Add the new elements to your `els` constant:**

```javascript
      // Add these inside the 'els' constant block
      perspective: document.getElementById('perspective'),
      animToggle: document.getElementById('anim-toggle'),
      animTime: document.getElementById('anim-time'),
      animAngle: document.getElementById('anim-angle')

```

**Replace the entire `render()` function with this updated version:**

```javascript
      function render() {
        const { vertices, faces } = parseOBJ(els.objInput.value);
        if (!vertices.length) return;

        // 1. Calculate center and maximum radius for constant scaling
        let cx = 0, cy = 0, cz = 0;
        vertices.forEach(v => { cx += v[0]; cy += v[1]; cz += v[2]; });
        cx /= vertices.length; cy /= vertices.length; cz /= vertices.length;

        let maxRadiusSq = 0;
        vertices.forEach(v => {
          let dx = v[0] - cx, dy = v[1] - cy, dz = v[2] - cz;
          maxRadiusSq = Math.max(maxRadiusSq, dx * dx + dy * dy + dz * dz);
        });
        const maxRadius = Math.sqrt(maxRadiusSq) || 1;

        const canvasSize = parseInt(els.cNum.value);
        const contentSize = parseInt(els.contNum.value);
        const op = parseFloat(els.opacity.value).toFixed(1);
        const sw = parseFloat(els.swWidth.value).toFixed(1);

        els.opVal.textContent = op;
        els.swVal.textContent = sw;

        const scale = contentSize / (maxRadius * 2);
        const offset = { x: canvasSize / 2, y: canvasSize / 2 };
        
        // Perspective logic
        const usePerspective = els.perspective.checked;
        const cameraDistance = maxRadius * 4; 

        // Toggle Animation Logic
        const doAnim = els.animToggle.checked;

        if (!doAnim) {
          // --- STATIC RENDERING ---
          const rotatedVertices = vertices.map(v => {
            let x = v[0] - cx, y = v[1] - cy, z = v[2] - cz;
            let rx = x * rotMatrix[0] + y * rotMatrix[1] + z * rotMatrix[2];
            let ry = x * rotMatrix[3] + y * rotMatrix[4] + z * rotMatrix[5];
            let rz = x * rotMatrix[6] + y * rotMatrix[7] + z * rotMatrix[8];
            return [rx, ry, rz];
          });

          let minX = Infinity, maxX = -Infinity, minY = Infinity, maxY = -Infinity;

          const projected = rotatedVertices.map(v => {
            let pFactor = usePerspective ? (cameraDistance / (cameraDistance - v[2])) : 1;
            let px = v[0] * scale * pFactor + offset.x;
            let py = -v[1] * scale * pFactor + offset.y;
            
            // Track bounding box for dynamic content-size updating
            minX = Math.min(minX, px); maxX = Math.max(maxX, px);
            minY = Math.min(minY, py); maxY = Math.max(maxY, py);

            return { x: px, y: py, z: v[2] };
          });
          
          window._lastBoundingBox = { minX, maxX, minY, maxY };

          const polys = [];
          faces.forEach(fIdx => {
            const pts = fIdx.map(i => projected[i]);
            let cp = 0;
            for (let i = 0; i < pts.length; i++) {
              let j = (i + 1) % pts.length;
              cp += pts[i].x * pts[j].y - pts[j].x * pts[i].y;
            }
            const isFront = cp < 0;

            if (isFront && !els.showFront.checked) return;
            if (!isFront && !els.showBack.checked) return;

            const avgZ = pts.reduce((sum, p) => sum + p.z, 0) / pts.length;
            polys.push({ pts, avgZ, sides: pts.length });
          });

          polys.sort((a, b) => a.avgZ - b.avgZ);

          let svg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${canvasSize} ${canvasSize}" width="${canvasSize}" height="${canvasSize}" stroke-linejoin="round">\n`;
          polys.forEach(p => {
            const pointsStr = p.pts.map(pt => `${pt.x.toFixed(2)},${pt.y.toFixed(2)}`).join(' ');
            const fill = document.getElementById(`hex-${p.sides}`)?.value || '#ccc';
            svg += `  <polygon points="${pointsStr}" fill="${fill}" fill-opacity="${op}" stroke="${els.swColor.value}" stroke-width="${sw}" />\n`;
          });
          svg += `</svg>`;

          els.container.innerHTML = svg;
          const bytes = new Blob([svg]).size;
          els.fileSize.textContent = `File Size: ${bytes.toLocaleString()} Bytes`;
          window._lastSvg = svg;

        } else {
          // --- ANIMATION RENDERING ---
          const T = Math.max(0.1, Math.min(48.0, parseFloat(els.animTime.value) || 7.0)).toFixed(1);
          const fps = 12;
          const totalFrames = Math.round(T * fps);
          const alpha = parseInt(els.animAngle.value) || 120;
          const alphaRad = alpha * Math.PI / 180;
          
          // Axis of rotation perpendicular to drag direction
          const axisX = -Math.sin(alphaRad);
          const axisY = Math.cos(alphaRad);

          let startAssigned = false;
          let animPolys = faces.map(() => ({ pointsValues: [], sets: [], avgZ: 0, initialIsFront: false, currentVis: false, sides: 0 }));

          for (let i = 0; i <= totalFrames; i++) {
            let angle = (i / totalFrames) * 2 * Math.PI;

            // Compute rotation matrix for this frame
            const c = Math.cos(angle), s = Math.sin(angle), t = 1 - c;
            const rAnim = [
                t*axisX*axisX + c,       t*axisX*axisY,           s*axisY,
                t*axisX*axisY,           t*axisY*axisY + c,       -s*axisX,
                -s*axisY,                s*axisX,                 c
            ];
            const curRot = multiplyMatrix(rAnim, rotMatrix);

            const frameProjected = vertices.map(v => {
              let x = v[0] - cx, y = v[1] - cy, z = v[2] - cz;
              let rx = x * curRot[0] + y * curRot[1] + z * curRot[2];
              let ry = x * curRot[3] + y * curRot[4] + z * curRot[5];
              let rz = x * curRot[6] + y * curRot[7] + z * curRot[8];

              let pFactor = usePerspective ? (cameraDistance / (cameraDistance - rz)) : 1;
              let px = rx * scale * pFactor + offset.x;
              let py = -ry * scale * pFactor + offset.y;
              return { x: px, y: py, z: rz };
            });

            faces.forEach((fIdx, fIndex) => {
              const pts = fIdx.map(idx => frameProjected[idx]);
              let cp = 0;
              for (let j = 0; j < pts.length; j++) {
                let k = (j + 1) % pts.length;
                cp += pts[j].x * pts[k].y - pts[k].x * pts[j].y;
              }
              const isFront = cp < 0;

              const pointsStr = pts.map(pt => `${pt.x.toFixed(2)},${pt.y.toFixed(2)}`).join(' ');
              animPolys[fIndex].pointsValues.push(pointsStr);
              animPolys[fIndex].sides = pts.length;

              if (i === 0) {
                // Initialize start values at frame 0
                animPolys[fIndex].initialIsFront = isFront;
                animPolys[fIndex].avgZ = pts.reduce((sum, p) => sum + p.z, 0) / pts.length;
                animPolys[fIndex].currentVis = isFront;
                
                let initialVisStr = isFront ? "visible" : "hidden";
                if (!startAssigned) {
                  animPolys[fIndex].sets.push(`<set id="start" attributeName="visibility" to="${initialVisStr}" begin="0;start.begin+${T}s" />`);
                  startAssigned = true;
                } else {
                  animPolys[fIndex].sets.push(`<set attributeName="visibility" to="${initialVisStr}" begin="start.begin" />`);
                }
              } else {
                // Calculate visibility changes based on strict time boundaries
                if (isFront !== animPolys[fIndex].currentVis) {
                  animPolys[fIndex].currentVis = isFront;
                  let visValue = isFront ? "visible" : "hidden";
                  let timeVal = (i / fps).toFixed(2);
                  animPolys[fIndex].sets.push(`<set attributeName="visibility" to="${visValue}" begin="start.begin+${timeVal}s" />`);
                }
              }
            });
          }

          // Depth sort based on initial state Z
          animPolys.sort((a, b) => a.avgZ - b.avgZ);

          let svg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${canvasSize} ${canvasSize}" width="${canvasSize}" height="${canvasSize}" stroke-linejoin="round">\n`;
          animPolys.forEach(p => {
            const fill = document.getElementById(`hex-${p.sides}`)?.value || '#ccc';
            svg += `  <polygon fill="${fill}" fill-opacity="${op}" stroke="${els.swColor.value}" stroke-width="${sw}">\n`;
            svg += `    <animate attributeName="points" values="${p.pointsValues.join(';')}" dur="${T}s" begin="start.begin" repeatCount="indefinite" />\n`;
            p.sets.forEach(setTag => {
              svg += `    ${setTag}\n`;
            });
            svg += `  </polygon>\n`;
          });
          svg += `</svg>`;

          els.container.innerHTML = svg;
          const bytes = new Blob([svg]).size;
          els.fileSize.textContent = `File Size: ${bytes.toLocaleString()} Bytes`;
          window._lastSvg = svg;
        }
      }

```

**Replace your mouse/touch Drag logic and add the new event listeners:**
Locate `let isDragging = false;` and replace the drag section and add listeners up until the `render();` call:

```javascript
      let isDragging = false;
      let lastMouseX = 0;
      let lastMouseY = 0;
      let startDragX = 0;
      let startDragY = 0;

      els.container.style.cursor = 'grab';

      function startDrag(e) {
        isDragging = true;
        els.container.style.cursor = 'grabbing';
        lastMouseX = e.clientX || (e.touches && e.touches[0].clientX);
        lastMouseY = e.clientY || (e.touches && e.touches[0].clientY);
        startDragX = lastMouseX;
        startDragY = lastMouseY;
      }

      function onDrag(e) {
        if (!isDragging) return;
        if (e.cancelable) e.preventDefault();

        const currentX = e.clientX || (e.touches && e.touches[0].clientX);
        const currentY = e.clientY || (e.touches && e.touches[0].clientY);
        const deltaX = currentX - lastMouseX;
        const deltaY = currentY - lastMouseY;
        const angleY = deltaX * 0.01;
        const angleX = deltaY * 0.01;

        const cosX = Math.cos(angleX), sinX = Math.sin(angleX);
        const cosY = Math.cos(angleY), sinY = Math.sin(angleY);

        const rotXMat = [1, 0, 0, 0, cosX, -sinX, 0, sinX, cosX];
        const rotYMat = [cosY, 0, sinY, 0, 1, 0, -sinY, 0, cosY];

        const deltaMat = multiplyMatrix(rotYMat, rotXMat);
        rotMatrix = multiplyMatrix(deltaMat, rotMatrix);

        lastMouseX = currentX;
        lastMouseY = currentY;

        render();
      }

      function endDrag() {
        if (!isDragging) return;
        isDragging = false;
        els.container.style.cursor = 'grab';

        // Recalculate content-size based on current bounding box 
        if (window._lastBoundingBox) {
          const { minX, maxX, minY, maxY } = window._lastBoundingBox;
          const size = Math.round(Math.max(maxX - minX, maxY - minY));
          if (size > 0 && !isNaN(size)) {
            els.contNum.value = size;
            els.contRange.value = Math.min(1200, Math.max(60, size));
          }
        }

        // Calculate and update rotation angle based on swipe vector
        if (lastMouseX !== startDragX || lastMouseY !== startDragY) {
          const dx = lastMouseX - startDragX;
          const dy = lastMouseY - startDragY;
          let angle = Math.round(Math.atan2(dy, dx) * 180 / Math.PI);
          els.animAngle.value = angle;
          if (els.animToggle.checked) render();
        }
      }

      // --- New UI Event Listeners ---
      els.perspective.addEventListener('change', render);
      els.animToggle.addEventListener('change', render);
      els.animAngle.addEventListener('change', render);
      
      // Rotation time triggers only on blur as requested
      els.animTime.addEventListener('blur', () => {
        let val = parseFloat(els.animTime.value);
        if (isNaN(val) || val < 0.1) val = 0.1;
        if (val > 48.0) val = 48.0;
        els.animTime.value = val.toFixed(1);
        render();
      });

```
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
一枝独秀
一枝独秀
帖子: 5538
注册时间: 2022年 11月 18日 17:36
为圈友点赞: 179 次
被圈友点赞: 207 次
联系:

Re: 正在開發新遊戲:星•球

帖子 ejsoon »

maketheball17

增加導出三維旋轉動畫功能!
maketheball17.7z
(6.67 KiB) 已下载 2 次
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
一枝独秀
一枝独秀
帖子: 5538
注册时间: 2022年 11月 18日 17:36
为圈友点赞: 179 次
被圈友点赞: 207 次
联系:

Re: 正在開發新遊戲:星•球

帖子 ejsoon »

修正:
一,轉動svg-container放開後,content size的值改得不對,它的意義應該是距離最遠的點,參考當其改動時的邏輯。

二,rotation angle僅當「旋轉動畫」打勾時,點擊svg-container放開才會改變它的值,且這時點擊svg-container拖動將不會再有實時的拖動效果。

三,當生成旋轉動畫時,前面的顯示與後面的隱藏仍然存在肉眼可見的偏差,嘗試在js代碼中增加一個變量表示set的時間保留小數後幾位,默認四位。同時檢查下還有哪些地方可以調整和改進。(當前面切換為後面,或後面切換為前面的時機,應是這個面的所有點都共線的時候。)

只需給出所要修改的地方,所有的代碼和注釋都要使用英文。



gemini pro加油!

代码: 全选

Here are the specific code modifications to address all three of your requirements. All code and comments are provided in English.

### 1 & 2. Modification for `onDrag` and `endDrag` functions

This removes the incorrect content size recalculation, stops real-time dragging when animation is checked, and ensures the angle is only updated when the animation toggle is enabled.

Replace your existing `onDrag` and `endDrag` functions with the following:

```javascript
      function onDrag(e) {
        if (!isDragging) return;
        if (e.cancelable) e.preventDefault();

        // 2. Do not apply real-time rotation if animation is enabled
        if (els.animToggle.checked) return;

        const currentX = e.clientX || (e.touches && e.touches[0].clientX);
        const currentY = e.clientY || (e.touches && e.touches[0].clientY);
        const deltaX = currentX - lastMouseX;
        const deltaY = currentY - lastMouseY;
        const angleY = deltaX * 0.01;
        const angleX = deltaY * 0.01;

        const cosX = Math.cos(angleX),
          sinX = Math.sin(angleX);
        const cosY = Math.cos(angleY),
          sinY = Math.sin(angleY);

        const rotXMat = [1, 0, 0, 0, cosX, -sinX, 0, sinX, cosX];
        const rotYMat = [cosY, 0, sinY, 0, 1, 0, -sinY, 0, cosY];

        const deltaMat = multiplyMatrix(rotYMat, rotXMat);
        rotMatrix = multiplyMatrix(deltaMat, rotMatrix);

        lastMouseX = currentX;
        lastMouseY = currentY;

        render();
      }

      function endDrag() {
        if (!isDragging) return;
        isDragging = false;
        els.container.style.cursor = 'grab';

        // 1. Removed incorrect content-size recalculation from bounding box.
        // Content size should act as a constant maximum radius scaling factor,
        // and auto-updating it from 2D bounding boxes causes scale issues.

        // 2. Calculate and update rotation angle based on swipe vector ONLY when animating
        if (els.animToggle.checked) {
          if (lastMouseX !== startDragX || lastMouseY !== startDragY) {
            const dx = lastMouseX - startDragX;
            const dy = lastMouseY - startDragY;
            let angle = Math.round((Math.atan2(dy, dx) * 180) / Math.PI);
            els.animAngle.value = angle;
            render();
          }
        }
      }

```

### 3. Modification for Animation Rendering (Visibility Glitches & Precision)

This introduces the precision variable and uses linear interpolation of the cross-product to find the exact sub-frame time when the polygon points become collinear (when it flips), removing the visual stuttering.

In your `render()` function, update the `// --- ANIMATION RENDERING ---` block as follows:

```javascript
        } else {
          // --- ANIMATION RENDERING ---
          const T = Math.max(0.1, Math.min(48.0, parseFloat(els.animTime.value) || 7.0)).toFixed(1);
          const fps = 12;
          const totalFrames = Math.round(T * fps);
          const alpha = parseInt(els.animAngle.value) || 120;
          const alphaRad = (alpha * Math.PI) / 180;

          // 3. Variable to control the decimal precision of <set> time values
          const setTimePrecision = 4;

          // Axis of rotation perpendicular to drag direction
          const axisX = -Math.sin(alphaRad);
          const axisY = Math.cos(alphaRad);

          let startAssigned = false;
          let animPolys = faces.map(() => ({
            pointsValues: [],
            sets: [],
            avgZ: 0,
            initialIsFront: false,
            currentVis: false,
            sides: 0,
            lastCp: 0 // Added to track cross-product for interpolation
          }));

          for (let i = 0; i <= totalFrames; i++) {
            let angle = (i / totalFrames) * 2 * Math.PI;

            // Compute rotation matrix for this frame
            const c = Math.cos(angle),
              s = Math.sin(angle),
              t = 1 - c;
            const rAnim = [
              t * axisX * axisX + c,
              t * axisX * axisY,
              s * axisY,
              t * axisX * axisY,
              t * axisY * axisY + c,
              -s * axisX,
              -s * axisY,
              s * axisX,
              c
            ];
            const curRot = multiplyMatrix(rAnim, rotMatrix);

            const frameProjected = vertices.map(v => {
              let x = v[0] - cx,
                y = v[1] - cy,
                z = v[2] - cz;
              let rx = x * curRot[0] + y * curRot[1] + z * curRot[2];
              let ry = x * curRot[3] + y * curRot[4] + z * curRot[5];
              let rz = x * curRot[6] + y * curRot[7] + z * curRot[8];

              let pFactor = usePerspective ? cameraDistance / (cameraDistance - rz) : 1;
              let px = rx * scale * pFactor + offset.x;
              let py = -ry * scale * pFactor + offset.y;
              return { x: px, y: py, z: rz };
            });

            faces.forEach((fIdx, fIndex) => {
              const pts = fIdx.map(idx => frameProjected[idx]);
              let cp = 0;
              for (let j = 0; j < pts.length; j++) {
                let k = (j + 1) % pts.length;
                cp += pts[j].x * pts[k].y - pts[k].x * pts[j].y;
              }
              const isFront = cp < 0;

              const pointsStr = pts.map(pt => `${pt.x.toFixed(2)},${pt.y.toFixed(2)}`).join(' ');
              animPolys[fIndex].pointsValues.push(pointsStr);
              animPolys[fIndex].sides = pts.length;

              if (i === 0) {
                // Initialize start values at frame 0
                animPolys[fIndex].initialIsFront = isFront;
                animPolys[fIndex].avgZ = pts.reduce((sum, p) => sum + p.z, 0) / pts.length;
                animPolys[fIndex].currentVis = isFront;
                animPolys[fIndex].lastCp = cp; // Store initial cross product

                let initialVisStr = isFront ? 'visible' : 'hidden';
                if (!startAssigned) {
                  animPolys[fIndex].sets.push(
                    `<set id="start" attributeName="visibility" to="${initialVisStr}" begin="0;start.begin+${T}s" />`
                  );
                  startAssigned = true;
                } else {
                  animPolys[fIndex].sets.push(
                    `<set attributeName="visibility" to="${initialVisStr}" begin="start.begin" />`
                  );
                }
              } else {
                // 3. Interpolate the exact moment the cross product passes through 0 (collinear points)
                if (isFront !== animPolys[fIndex].currentVis) {
                  animPolys[fIndex].currentVis = isFront;
                  let visValue = isFront ? 'visible' : 'hidden';
                  
                  let lastCp = animPolys[fIndex].lastCp;
                  // Calculate the fractional time between previous frame and current frame
                  let fraction = (Math.abs(lastCp - cp) > 1e-6) ? lastCp / (lastCp - cp) : 0.5;
                  
                  // Calculate precise time with increased decimal places
                  let exactTime = ((i - 1 + fraction) * (T / totalFrames)).toFixed(setTimePrecision);
                  
                  animPolys[fIndex].sets.push(
                    `<set attributeName="visibility" to="${visValue}" begin="start.begin+${exactTime}s" />`
                  );
                }
                animPolys[fIndex].lastCp = cp; // Update cp for next frame comparison
              }
            });
          }

```
https://ejsoon.vip/
弈趣極光:享受思維樂趣
回复

在线用户

正浏览此版面之用户: ejsoon 和 0 访客