// Produto3D: o herói do filme. Um produto 3D de verdade (raymarching em JS puro, ver raymarch.ts) em // estúdio escuro: luz de borda quente/fria atrás, softbox fraca em cima, sombra de contato e REFLEXO // num chão polido. O canvas é transparente: o breu e o feixe de luz vêm do Fundo. // A câmera orbita/aproxima por chaves; `explodir` abre a vista explodida (cada peça vai pro seu `abre`) // e `cascata` atrasa uma peça da outra, pra elas encaixarem uma a uma. import React, {useLayoutEffect, useRef} from 'react'; import {AbsoluteFill, useCurrentFrame} from 'remotion'; import {ALTURA, COR, CURVAS, LARGURA, NomeCurva, seg, trilha} from '../base'; import {desenhar} from '../lib/raymarch'; import {M3, Peca, UsoModelo, V3, aplicar, expandir, mul, rotacao, somar} from '../lib/modelos'; const MAXP = 16; type Chaves = [number, number][] | number; import type {ChaveCamera} from '../lib/produto3d-apoio'; export type {ChaveCamera}; export type Animacao = {grupo: string; de: number; ate: number; pos?: V3; rot?: V3; pivo?: V3; curva?: NomeCurva}; export type Produto3DProps = { modelo?: UsoModelo[]; pecas?: Peca[]; /** cor do corpo anodizado */ cor?: string; /** vista explodida: chaves [[quadro, 0..1]] (0 = montado, 1 = aberto) */ explodir?: [number, number][]; /** curva da vista explodida (padrão `suave`; use `encaixe` pra fechar seco) */ curvaExplodir?: NomeCurva; /** atraso em quadros entre uma peça e a próxima (as de fora andam primeiro) */ cascata?: number; animar?: Animacao[]; /** giro do produto inteiro em graus: Y (giro), X (inclinar), Z (rolar). Número ou chaves */ giro?: Chaves; inclinar?: Chaves; rolar?: Chaves; centro?: V3; mover?: {em: number; pos: V3}[]; flutuar?: number; camera?: ChaveCamera[]; /** campo de visão vertical em graus (lente longa de produto: 20–30) */ lente?: number; /** altura do chão, força da sombra (0–1) e do reflexo no chão polido (0–0,6) */ chao?: number; sombra?: number; reflexo?: number; /** cor e força (chaves 0–1) do anel de acento */ acento?: string; brilho?: Chaves; /** luz geral (chaves 0–1): 0 = breu, revela o produto */ luz?: Chaves; /** força da luz de borda (chaves 0–1,5) */ borda?: Chaves; opacidade?: Chaves; qualidade?: number; }; import {hexRgb, TIPOS, MATS, MAT_COR, camera} from '../lib/produto3d-apoio'; export const Produto3D: React.FC = ({ modelo, pecas = [], cor, explodir, curvaExplodir = 'suave', cascata = 2, animar = [], giro, inclinar, rolar, centro = [0, 0, 0], mover, flutuar = 0.02, camera: chavesCam = [{em: 0}], lente = 24, chao = -1.1, sombra = 0.6, reflexo = 0.38, acento = COR.acento, brilho = 0.15, luz = 1, borda = 1, opacidade, qualidade = 1, }) => { const f = useCurrentFrame(); const ref = useRef(null); const dpr = typeof window === 'undefined' ? 1 : Math.min(1, window.devicePixelRatio || 1); const W = Math.round(LARGURA * dpr * qualidade), H = Math.round(ALTURA * dpr * qualidade); const todas = [...expandir(modelo), ...pecas].slice(0, MAXP) as (Peca & {rotM?: M3})[]; // ordem da cascata: as peças que andam mais longe saem primeiro e voltam por último const grupos = [...new Map(todas.map((pc) => [pc.grupo ?? '', pc.abre ? Math.hypot(...pc.abre) : 0] as [string, number])).entries()] .sort((a, b) => b[1] - a[1]).map((g) => g[0]); const Rraiz = rotacao([trilha(f, inclinar, 0), trilha(f, giro, 0), trilha(f, rolar, 0)]); const desl: V3 = mover?.length ? [0, 1, 2].map((i) => trilha(f, mover.map((m) => [m.em, m.pos[i]] as [number, number]), 0)) as V3 : [0, 0, 0]; const fy = Math.sin((f / 30) * Math.PI * 2 / 3.4) * flutuar; const mundo = todas.map((pc) => { let pos: V3 = pc.pos ?? [0, 0, 0]; let R: M3 = pc.rotM ?? rotacao(pc.rot); if (explodir && pc.abre) { const k = trilha(f - grupos.indexOf(pc.grupo ?? '') * cascata, explodir, 0, curvaExplodir); pos = somar(pos, [pc.abre[0] * k, pc.abre[1] * k, pc.abre[2] * k]); } for (const a of animar) { if (!pc.grupo || !(pc.grupo === a.grupo || pc.grupo.startsWith(a.grupo + '.'))) continue; const t = CURVAS[a.curva ?? 'suave'](seg(f, a.de, a.ate)); if (t <= 0) continue; if (a.rot) { const Ra = rotacao([a.rot[0] * t, a.rot[1] * t, a.rot[2] * t]); const pv = a.pivo ?? [0, 0, 0]; pos = somar(pv, aplicar(Ra, [pos[0] - pv[0], pos[1] - pv[1], pos[2] - pv[2]])); R = mul(Ra, R); } if (a.pos) pos = somar(pos, [a.pos[0] * t, a.pos[1] * t, a.pos[2] * t]); } pos = somar(somar(centro, aplicar(Rraiz, [pos[0] - centro[0], pos[1] - centro[1], pos[2] - centro[2]])), [desl[0], desl[1] + fy, desl[2]]); R = mul(Rraiz, R); return {pc, pos, R}; }); const ext = (pc: Peca) => { const [a, b, c] = pc.tam; const r = pc.raio ?? 0; if (pc.forma === 'capsula') return b / 2 + Math.max(a, c); if (pc.forma === 'anel') return a + b; if (pc.forma === 'cilindro' || pc.forma === 'estriado') return Math.hypot(a, b) + r; return Math.hypot(a, b, c) + r; }; const sol = mundo.filter((m) => (m.pc.uniao ?? 0) >= 0); const cx = sol.reduce((s, m) => somar(s, m.pos), [0, 0, 0] as V3).map((v) => v / Math.max(1, sol.length)) as V3; const raio = sol.reduce((r, m) => Math.max(r, Math.hypot(m.pos[0] - cx[0], m.pos[1] - cx[1], m.pos[2] - cx[2]) + ext(m.pc)), 0.1) + 0.05; const cam = camera(f, chavesCam); const el = (cam.elevacao * Math.PI) / 180, az = (cam.orbita * Math.PI) / 180; const olho: V3 = [cam.alvo[0] + cam.distancia * Math.cos(el) * Math.sin(az), cam.alvo[1] + cam.distancia * Math.sin(el), cam.alvo[2] + cam.distancia * Math.cos(el) * Math.cos(az)]; const forca = trilha(f, brilho, 0); const luzK = trilha(f, luz, 1), bordaK = trilha(f, borda, 1); const op = trilha(f, opacidade, 1); useLayoutEffect(() => { const cv = ref.current; if (!cv) return; const ctx = cv.getContext('2d'); if (!ctx) return; const N = mundo.length; const tipo = new Int8Array(N), pos = new Float64Array(N * 3), tam = new Float64Array(N * 3), rr = new Float64Array(N), rot = new Float64Array(N * 9), mat = new Int8Array(N), corA = new Float64Array(N * 3), un = new Float64Array(N), corte = new Float64Array(N * 2), ex = new Float64Array(N); mundo.forEach(({pc, pos: p, R}, i) => { tipo[i] = TIPOS[pc.forma]; pos.set(p, i * 3); tam.set(pc.tam, i * 3); rr[i] = pc.raio ?? 0; rot.set(R, i * 9); const m = pc.material ?? 'anodizado'; mat[i] = MATS[m]; corA.set(hexRgb(pc.cor ?? (m === 'anodizado' ? cor ?? MAT_COR[m] : MAT_COR[m])), i * 3); un[i] = pc.uniao ?? 0; corte.set(pc.corte ?? [0, 0], i * 2); ex[i] = ext(pc) + Math.max(0, pc.uniao ?? 0) + 0.01; }); desenhar(ctx, W, H, { n: N, tipo, pos, tam, raio: rr, rot, mat, cor: corA, un, corte, ext: ex, esfera: [cx[0], cx[1], cx[2], raio], chao, sombra, reflexo, acento: hexRgb(acento), brilho: forca, luz: luzK, borda: bordaK, }, {olho, alvo: cam.alvo, tan: Math.tan(((lente / 2) * Math.PI) / 180)}); }); return ; };