cozyclay 3.0.3 → 3.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +93 -0
- package/INSTALL_FOR_AGENTS.md +146 -0
- package/README.md +74 -192
- package/THIRD_PARTY_NOTICES.md +0 -21
- package/bin/agent/agent-routes.mjs +29 -62
- package/bin/agent/agent-runner.mjs +5 -3
- package/bin/agent/main-agent.mjs +70 -0
- package/bin/agent/main-prompt.mjs +17 -0
- package/bin/agent/plan-tool.mjs +114 -0
- package/bin/agent/script-tool.mjs +2 -0
- package/bin/agent/sculpt-sketch.mjs +190 -0
- package/bin/agent/session-store.mjs +4 -3
- package/bin/agent/sketch-code.mjs +151 -0
- package/bin/agent/sketch-sandbox.mjs +102 -0
- package/bin/agent/studio-prompt.mjs +1 -1
- package/bin/agent/studio-task-routes.mjs +185 -0
- package/bin/agent/studio-tasks.mjs +156 -0
- package/bin/agent/studio-tools.mjs +3 -2
- package/bin/agent/studio-turn-transaction.mjs +76 -0
- package/bin/agent/turn-frames.mjs +4 -0
- package/bin/cozyclay.mjs +5 -7
- package/dist/app/index.html +7 -6
- package/dist/assets/TaskBoard-ngdqiu7u.js +1 -0
- package/dist/assets/app-BlwOpest.css +1 -0
- package/dist/assets/app-DmR7jBkz.js +5144 -0
- package/dist/assets/compile-worker-D0pKioa0.js +5 -0
- package/dist/assets/landing-y3EqDgSZ.js +1 -0
- package/dist/assets/modulepreload-polyfill-P2Xu9kJm.js +1 -0
- package/dist/assets/motion3-compile-BB3_1-EB.js +1 -0
- package/dist/assets/scene-asset-sync-CBx7XRti.css +1 -0
- package/dist/assets/scene-asset-sync-Cgdtr1Yw.js +13 -0
- package/dist/assets/task-board-ErQvfoaz.js +14 -0
- package/dist/assets/workflow-zQM-EKW1.js +179 -0
- package/dist/assets/x-bot-tpose-body-BusyR3eg.json +1 -0
- package/dist/assets/y-bot-tpose-body-Dy1VKChZ.json +1 -0
- package/dist/cozyclay-package.json +1 -1
- package/dist/greybox-to-video/index.html +2 -3
- package/dist/index.html +7 -57
- package/dist/llms.txt +2 -1
- package/dist/previs-software/index.html +2 -2
- package/dist/privacy/index.html +3 -7
- package/dist/seedance-camera-control/index.html +2 -2
- package/dist/sitemap.xml +5 -5
- package/dist/sw.js +5 -1
- package/dist/thumbs/blank-stage-dark.webp +0 -0
- package/dist/thumbs/blank-stage-light.webp +0 -0
- package/dist/workflow/index.html +5 -4
- package/mcp/mesh-file.mjs +3 -1
- package/mcp/server.mjs +3 -0
- package/mcp/tool-handlers.mjs +2 -2
- package/package.json +5 -3
- package/src/App.jsx +51 -282
- package/src/analytics.js +3 -148
- package/src/app-stage.jsx +1 -1
- package/src/asset-pane.jsx +5 -10
- package/src/auto-flat.js +10 -0
- package/src/bus-tasks.js +11 -5
- package/src/command-bus.js +1 -1
- package/src/commands/elements.js +4 -3
- package/src/commands/motion3-compile.js +51 -27
- package/src/commands/motion3.js +3 -3
- package/src/commands/objects.js +1 -1
- package/src/commands/project.js +1 -1
- package/src/commands/shot.js +16 -4
- package/src/controls.jsx +0 -11
- package/src/cuts.js +11 -0
- package/src/domains/cast.js +3 -2
- package/src/domains/motion.js +3 -10
- package/src/domains/playback.js +32 -0
- package/src/domains/scenes.js +3 -22
- package/src/domains/shots.js +5 -5
- package/src/dualview.jsx +11 -15
- package/src/hierarchy-panel.css +18 -4
- package/src/hierarchy-panel.jsx +12 -11
- package/src/kits/held.js +3 -2
- package/src/light-blockers.js +2 -2
- package/src/main.jsx +5 -0
- package/src/material-look.js +189 -0
- package/src/material-presets.js +78 -0
- package/src/material-textures.js +157 -0
- package/src/motion/ground.js +5 -14
- package/src/motion/playback-frame-store.js +79 -0
- package/src/motion/playback-probe.js +16 -0
- package/src/motion/playback-subject.js +7 -0
- package/src/motion/timeline.css +43 -5
- package/src/motion/timeline.jsx +4 -3
- package/src/motion3/body.js +37 -5
- package/src/motion3/carried.js +11 -3
- package/src/motion3/compile-job.js +1 -1
- package/src/motion3/compiler/compile.js +7 -1
- package/src/motion3/compiler/finger-close.js +137 -0
- package/src/motion3/compiler/limbs.js +18 -3
- package/src/motion3/compiler/pose.js +6 -0
- package/src/motion3/compiler/validate-objects.js +17 -2
- package/src/motion3/compiler/validate-parts.js +2 -1
- package/src/motion3/compiler/vocab.js +4 -4
- package/src/motion3/describe.js +2 -2
- package/src/motion3/fix/index.js +5 -3
- package/src/motion3/flight.js +38 -0
- package/src/motion3/free-motion.js +10 -7
- package/src/motion3/load.js +125 -0
- package/src/motion3/mass.js +23 -0
- package/src/motion3/object-keys.js +11 -6
- package/src/motion3/object-pose.js +11 -3
- package/src/motion3/pipeline.js +3 -0
- package/src/motion3/qa/explain.js +3 -1
- package/src/motion3/qa/grasp-checks.js +123 -0
- package/src/motion3/qa/hints-grasp.js +71 -0
- package/src/motion3/qa/index.js +3 -1
- package/src/motion3/qa/object-checks.js +7 -2
- package/src/motion3/qa/patch.js +2 -1
- package/src/motion3/qa/spec-checks.js +10 -1
- package/src/motion3/schema/fields.js +3 -3
- package/src/motion3/schema/topics.js +9 -8
- package/src/motion3/schema/types.js +4 -1
- package/src/motion3/seat.js +84 -0
- package/src/motion3/settle.js +11 -6
- package/src/motion3/shapes.js +70 -0
- package/src/motion3/skills/catch.js +71 -0
- package/src/motion3/skills/index.js +10 -3
- package/src/motion3/skills/info.js +13 -0
- package/src/motion3/skills/throw.js +72 -0
- package/src/object-catalog.jsx +0 -4
- package/src/object-gizmo.jsx +2 -1
- package/src/object-look.js +9 -5
- package/src/object-path.js +1 -1
- package/src/panels/ProjectPanel.jsx +42 -21
- package/src/panels/details.css +15 -4
- package/src/popover.js +37 -0
- package/src/posestudio.jsx +15 -10
- package/src/primitive-look.js +38 -0
- package/src/project-browser-art.jsx +19 -0
- package/src/project-browser.css +74 -121
- package/src/project-browser.jsx +36 -60
- package/src/props.jsx +24 -35
- package/src/render-passes.js +7 -1
- package/src/retired-kinds.js +37 -0
- package/src/scene-objects.js +5 -9
- package/src/scenes.js +4 -5
- package/src/sculpt-colours.js +65 -0
- package/src/sculpt-compare.js +13 -50
- package/src/sculpt-csg.js +112 -0
- package/src/sculpt-curve.js +123 -0
- package/src/sculpt-cutters.js +43 -0
- package/src/sculpt-detail.js +207 -0
- package/src/sculpt-geometry-loft-convex.js +100 -0
- package/src/sculpt-geometry-shapes.js +250 -0
- package/src/sculpt-geometry.js +33 -15
- package/src/sculpt-loft-rings.js +225 -0
- package/src/sculpt-noise.js +65 -0
- package/src/sculpt-part-fields.js +27 -0
- package/src/sculpt-recipe-read.js +65 -0
- package/src/sculpt-recipe.js +33 -75
- package/src/sculpt-run.js +1 -1
- package/src/sculpt-scatter-items.js +183 -0
- package/src/sculpt-scatter-shape.js +57 -0
- package/src/sculpt-scatter.js +124 -0
- package/src/sculpt-shapes-curved.js +211 -0
- package/src/sculpt-shapes-loft-convex.js +218 -0
- package/src/sculpt-shapes.js +138 -0
- package/src/sculpt-sketch-state.js +76 -0
- package/src/sculpt-sketch.jsx +124 -0
- package/src/sculpt-surface-fields.js +65 -0
- package/src/settings-menu.jsx +1 -20
- package/src/shell/BottomDock.jsx +9 -9
- package/src/shell/MenuBar.jsx +1 -9
- package/src/shell/PreferencesDialog.jsx +3 -4
- package/src/shell/ViewportToolbar.jsx +3 -3
- package/src/shell/add-camera-shot.js +11 -0
- package/src/shell/glass-regions.css +3 -2
- package/src/shell/glass.css +0 -5
- package/src/shell/lens-scrub-math.js +51 -0
- package/src/shell/lens-scrub.css +73 -0
- package/src/shell/lens-scrub.jsx +98 -0
- package/src/shell/mode.css +6 -5
- package/src/shell/preferences.css +1 -32
- package/src/shell/shell.css +18 -1
- package/src/shell/studio-shell-context.js +5 -0
- package/src/shot-frame.js +26 -0
- package/src/stage-canvas-rigs.jsx +7 -8
- package/src/stage-characters.jsx +26 -0
- package/src/stage-gizmo.js +43 -0
- package/src/stage-labels-ko.js +0 -6
- package/src/stage-startup-defaults.js +3 -9
- package/src/starter-scenes.js +5 -11
- package/src/studio-agent-commands.js +2 -2
- package/src/studio-agent-protocol.js +2 -7
- package/src/studio-app-binding.js +1 -1
- package/src/studio-elements.js +6 -2
- package/src/styles/themes.css +13 -0
- package/src/styles.css +6 -674
- package/src/text/text-object.jsx +1 -1
- package/src/ui.jsx +8 -16
- package/src/use-embed-bridge.js +3 -6
- package/src/use-render-activity.js +3 -1
- package/src/view-ghosts.jsx +5 -3
- package/src/viewport-visible-rect.js +37 -0
- package/src/workflow/AgentPanel.jsx +79 -46
- package/src/workflow/AgentPlanCard.jsx +41 -0
- package/src/workflow/AgentProgressCard.jsx +1 -1
- package/src/workflow/TaskBoard.jsx +102 -0
- package/src/workflow/TaskInline.jsx +114 -0
- package/src/workflow/agent-client.js +25 -36
- package/src/workflow/agent-panel.css +14 -1
- package/src/workflow/agent-plan.css +93 -0
- package/src/workflow/agent-plan.js +59 -0
- package/src/workflow/attachment-image.js +10 -3
- package/src/workflow/attachment-video.js +91 -0
- package/src/workflow/task-board.css +98 -0
- package/src/workflow/task-board.js +169 -0
- package/tools/check-eslint.mjs +2 -1
- package/tools/check-knip.mjs +2 -1
- package/tools/dev/pages/greybox-to-video.html +2 -3
- package/tools/dev/pages/previs-software.html +2 -2
- package/tools/dev/pages/privacy.html +3 -7
- package/tools/dev/pages/seedance-camera-control.html +2 -2
- package/tools/dev-full.mjs +11 -2
- package/tools/eslint-baseline.json +6 -6
- package/tools/knip-baseline.json +0 -11
- package/tools/loc-baseline.json +30 -31
- package/tools/metrics-snapshot.mjs +43 -5
- package/tools/render-start-thumbs.mjs +175 -0
- package/tools/run-tests.mjs +10 -17
- package/tools/sitemap.mjs +1 -1
- package/dist/assets/analytics-BibyE5ar.js +0 -6
- package/dist/assets/app-DhRJ-PBz.js +0 -5088
- package/dist/assets/app-f-fkV5uP.css +0 -1
- package/dist/assets/compile-worker-DqiA4AaN.js +0 -5
- package/dist/assets/first-shot-handoff-Qx7SKl38.js +0 -1
- package/dist/assets/landing-DYg87MDY.js +0 -1
- package/dist/assets/scene-asset-sync-DBYQBR0T.js +0 -20
- package/dist/assets/scene-asset-sync-_S8NQl5B.css +0 -1
- package/dist/assets/workflow-BSA22Ofr.js +0 -179
- package/dist/scenes/city-block.cclayproject +0 -1
- package/src/burn-in.js +0 -91
- package/src/camera-tutorial.jsx +0 -520
- package/src/facing-marks.js +0 -88
- package/src/first-shot-handoff.js +0 -44
- package/src/panels/motion.css +0 -197
- package/src/set-pieces.jsx +0 -183
- package/src/tutorial-analytics.js +0 -86
- package/src/tutorial-walk.js +0 -17
- package/src/use-camera-tutorial.js +0 -50
- package/src/use-semantic-state.js +0 -12
- package/tools/metrics/archive-traffic.mjs +0 -138
- package/tools/perf/ik-camera-drag.mjs +0 -138
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"app":"cozyclay","kind":"project","version":3,"name":"City Block","savedAt":1788959356489,"scenes":{"version":4,"activeSceneId":"scene-mt1b9laj-2","scenes":[{"id":"scene-mt1b9laj-2","name":"SCENE 01","objects":[{"id":"cube-2","name":"Cube 2","renderer":"cube","x":-7,"y":0,"z":-6,"rot":0,"rotX":0,"rotZ":0,"scaleX":5.2,"scaleY":13.5,"scaleZ":5.5,"color":"#c2c6c8","parent":null,"footprint":{"width":1,"depth":1},"height":1},{"id":"plane","name":"Plane","renderer":"plane","x":-6.5,"y":0,"z":-14.100000000000001,"rot":0,"rotX":0,"rotZ":0,"scaleX":7.2,"scaleY":1,"scaleZ":32,"color":"#c2c6c8","parent":null,"footprint":{"width":2,"depth":2},"height":0},{"id":"cube","name":"Cube","renderer":"cube","x":-4.5,"y":0,"z":0,"rot":0,"rotX":0,"rotZ":0,"scaleX":1.4,"scaleY":0.22,"scaleZ":32,"color":"#c2c6c8","parent":null,"footprint":{"width":1,"depth":1},"height":1},{"id":"cube-3","name":"Cube 3","renderer":"cube","x":4.5,"y":0,"z":0,"rot":0,"rotX":0,"rotZ":0,"scaleX":1.4,"scaleY":0.22,"scaleZ":32,"color":"#c2c6c8","parent":null,"footprint":{"width":1,"depth":1},"height":1},{"id":"cube-4","name":"Cube 4","renderer":"cube","x":-11.8,"y":0,"z":-14.9,"rot":0,"rotX":0,"rotZ":0,"scaleX":4.6,"scaleY":8,"scaleZ":5.2,"color":"#c2c6c8","parent":null,"footprint":{"width":1,"depth":1},"height":1},{"id":"cube-5","name":"Cube 5","renderer":"cube","x":-9.5,"y":0,"z":-5.5,"rot":0,"rotX":0,"rotZ":0,"scaleX":5.8,"scaleY":17,"scaleZ":6.2,"color":"#c2c6c8","parent":null,"footprint":{"width":1,"depth":1},"height":1},{"id":"cube-6","name":"Cube 6","renderer":"cube","x":-8,"y":0,"z":2,"rot":0,"rotX":0,"rotZ":0,"scaleX":4.5,"scaleY":9.5,"scaleZ":5.5,"color":"#c2c6c8","parent":null,"footprint":{"width":1,"depth":1},"height":1},{"id":"cube-7","name":"Cube 7","renderer":"cube","x":-10.2,"y":0,"z":8.5,"rot":0,"rotX":0,"rotZ":0,"scaleX":6,"scaleY":12.5,"scaleZ":5,"color":"#c2c6c8","parent":null,"footprint":{"width":1,"depth":1},"height":1},{"id":"cube-8","name":"Cube 8","renderer":"cube","x":-7.2,"y":0,"z":14,"rot":0,"rotX":0,"rotZ":0,"scaleX":4,"scaleY":6.5,"scaleZ":4.5,"color":"#c2c6c8","parent":null,"footprint":{"width":1,"depth":1},"height":1},{"id":"cylinder","name":"Cylinder","renderer":"cylinder","x":9,"y":0,"z":-12,"rot":0,"rotX":0,"rotZ":0,"scaleX":4.2,"scaleY":14,"scaleZ":4.2,"color":"#c2c6c8","parent":null,"footprint":{"width":1,"depth":1},"height":1},{"id":"cube-9","name":"Cube 9","renderer":"cube","x":8,"y":0,"z":-5,"rot":0,"rotX":0,"rotZ":0,"scaleX":4.5,"scaleY":7.5,"scaleZ":5.5,"color":"#c2c6c8","parent":null,"footprint":{"width":1,"depth":1},"height":1},{"id":"cube-10","name":"Cube 10","renderer":"cube","x":10.5,"y":0,"z":1.5,"rot":0,"rotX":0,"rotZ":0,"scaleX":6.2,"scaleY":11,"scaleZ":5,"color":"#c2c6c8","parent":null,"footprint":{"width":1,"depth":1},"height":1},{"id":"cube-11","name":"Cube 11","renderer":"cube","x":8,"y":0,"z":8,"rot":0,"rotX":0,"rotZ":0,"scaleX":4.8,"scaleY":8.5,"scaleZ":5.5,"color":"#c2c6c8","parent":null,"footprint":{"width":1,"depth":1},"height":1},{"id":"cube-12","name":"Cube 12","renderer":"cube","x":10.2,"y":0,"z":14,"rot":0,"rotX":0,"rotZ":0,"scaleX":6,"scaleY":15.5,"scaleZ":5,"color":"#c2c6c8","parent":null,"footprint":{"width":1,"depth":1},"height":1},{"id":"cube-13","name":"Cube 13","renderer":"cube","x":-15,"y":0,"z":-10,"rot":0,"rotX":0,"rotZ":0,"scaleX":4,"scaleY":9,"scaleZ":4,"color":"#c2c6c8","parent":null,"footprint":{"width":1,"depth":1},"height":1},{"id":"cube-14","name":"Cube 14","renderer":"cube","x":-15,"y":0,"z":3,"rot":0,"rotX":0,"rotZ":0,"scaleX":5,"scaleY":6,"scaleZ":5,"color":"#c2c6c8","parent":null,"footprint":{"width":1,"depth":1},"height":1},{"id":"cube-15","name":"Cube 15","renderer":"cube","x":15,"y":0,"z":-6,"rot":0,"rotX":0,"rotZ":0,"scaleX":5,"scaleY":10,"scaleZ":5,"color":"#c2c6c8","parent":null,"footprint":{"width":1,"depth":1},"height":1},{"id":"cube-16","name":"Cube 16","renderer":"cube","x":15,"y":0,"z":7,"rot":0,"rotX":0,"rotZ":0,"scaleX":4.5,"scaleY":7,"scaleZ":4.5,"color":"#c2c6c8","parent":null,"footprint":{"width":1,"depth":1},"height":1},{"id":"cube-17","name":"Cube 17","renderer":"cube","x":0,"y":4.3,"z":-7.5,"rot":0,"rotX":0,"rotZ":0,"scaleX":15,"scaleY":0.55,"scaleZ":2.4,"color":"#c2c6c8","parent":null,"footprint":{"width":1,"depth":1},"height":1},{"id":"cylinder-2","name":"Cylinder 2","renderer":"cylinder","x":-4.4,"y":0,"z":-7.5,"rot":0,"rotX":0,"rotZ":0,"scaleX":0.55,"scaleY":4.3,"scaleZ":0.55,"color":"#c2c6c8","parent":null,"footprint":{"width":1,"depth":1},"height":1},{"id":"cylinder-3","name":"Cylinder 3","renderer":"cylinder","x":4.4,"y":0,"z":-7.5,"rot":0,"rotX":0,"rotZ":0,"scaleX":0.55,"scaleY":4.3,"scaleZ":0.55,"color":"#c2c6c8","parent":null,"footprint":{"width":1,"depth":1},"height":1},{"id":"car","name":"Car","renderer":"car","x":-1.6,"y":0,"z":-7,"rot":0,"rotX":0,"rotZ":0,"scaleX":1,"scaleY":1,"scaleZ":1,"color":"#d98770","parent":null,"footprint":{"width":1.8,"depth":4.5},"height":1.4},{"id":"car-2","name":"Car 2","renderer":"car","x":1.5,"y":0,"z":-1.5,"rot":-180,"rotX":0,"rotZ":0,"scaleX":0.95,"scaleY":0.95,"scaleZ":0.95,"color":"#d98770","parent":null,"footprint":{"width":1.8,"depth":4.5},"height":1.4},{"id":"car-3","name":"Car 3","renderer":"car","x":-2.35,"y":0,"z":3.9,"rot":0,"rotX":0,"rotZ":0,"scaleX":1.05,"scaleY":1.05,"scaleZ":1.05,"color":"#d98770","parent":null,"footprint":{"width":1.8,"depth":4.5},"height":1.4},{"id":"car-4","name":"Car 4","renderer":"car","x":1.4,"y":0,"z":12,"rot":-180,"rotX":0,"rotZ":0,"scaleX":0.9,"scaleY":0.9,"scaleZ":0.9,"color":"#d98770","parent":null,"footprint":{"width":1.8,"depth":4.5},"height":1.4},{"id":"cylinder-4","name":"Cylinder 4","renderer":"cylinder","x":-3.45,"y":0,"z":-11,"rot":0,"rotX":0,"rotZ":0,"scaleX":0.11,"scaleY":5.5,"scaleZ":0.11,"color":"#c2c6c8","parent":null,"footprint":{"width":1,"depth":1},"height":1},{"id":"cylinder-5","name":"Cylinder 5","renderer":"cylinder","x":3.45,"y":0,"z":-7,"rot":0,"rotX":0,"rotZ":0,"scaleX":0.11,"scaleY":5.5,"scaleZ":0.11,"color":"#c2c6c8","parent":null,"footprint":{"width":1,"depth":1},"height":1},{"id":"cylinder-6","name":"Cylinder 6","renderer":"cylinder","x":-3.45,"y":0,"z":-2,"rot":0,"rotX":0,"rotZ":0,"scaleX":0.11,"scaleY":5.5,"scaleZ":0.11,"color":"#c2c6c8","parent":null,"footprint":{"width":1,"depth":1},"height":1},{"id":"cylinder-7","name":"Cylinder 7","renderer":"cylinder","x":3.45,"y":0,"z":3,"rot":0,"rotX":0,"rotZ":0,"scaleX":0.11,"scaleY":5.5,"scaleZ":0.11,"color":"#c2c6c8","parent":null,"footprint":{"width":1,"depth":1},"height":1},{"id":"cylinder-8","name":"Cylinder 8","renderer":"cylinder","x":-3.45,"y":0,"z":9,"rot":0,"rotX":0,"rotZ":0,"scaleX":0.11,"scaleY":5.5,"scaleZ":0.11,"color":"#c2c6c8","parent":null,"footprint":{"width":1,"depth":1},"height":1},{"id":"cylinder-9","name":"Cylinder 9","renderer":"cylinder","x":3.45,"y":0,"z":14,"rot":0,"rotX":0,"rotZ":0,"scaleX":0.11,"scaleY":5.5,"scaleZ":0.11,"color":"#c2c6c8","parent":null,"footprint":{"width":1,"depth":1},"height":1},{"id":"cylinder-10","name":"Cylinder 10","renderer":"cylinder","x":8,"y":8.5,"z":8,"rot":0,"rotX":0,"rotZ":0,"scaleX":1.1,"scaleY":1.5,"scaleZ":1.1,"color":"#c2c6c8","parent":null,"footprint":{"width":1,"depth":1},"height":1},{"id":"cone","name":"Cone","renderer":"cone","x":8,"y":10,"z":8,"rot":0,"rotX":0,"rotZ":0,"scaleX":1.4,"scaleY":1.2,"scaleZ":1.4,"color":"#c2c6c8","parent":null,"footprint":{"width":1,"depth":1},"height":1},{"id":"sphere","name":"Sphere","renderer":"sphere","x":4.2,"y":1.4,"z":-13.5,"rot":0,"rotX":0,"rotZ":0,"scaleX":2.2,"scaleY":2.2,"scaleZ":2.2,"color":"#c2c6c8","parent":null,"footprint":{"width":1,"depth":1},"height":1}],"shotDocument":{"version":4,"frameCount":432,"waypoints":[],"shots":[]},"stage":{"characters":[{"id":"char-a","model":"y-bot-tpose","x":0,"y":0,"z":0,"rot":0,"hidden":false,"tint":null,"pose":null,"identityImage":null,"scale":1,"subject":"a young woman in a tan coat","layer":{"waypoints":[],"promptClips":[]},"motionRef":null}],"hasCharSheet":false,"environmentImage":null,"shotAspect":"16:9","cameraPresetId":null,"sensorId":"fullFrame","keyLight":{"x":6,"y":9,"z":4,"intensity":1.12,"warmth":0.5}}}]},"workspace":{"hierarchyWidth":300,"sidebarWidth":380,"timelineHeight":224,"insetWidth":310,"insetHeight":310,"insetCollapsed":false,"planZoom":1},"poseLibrary":[],"assets":[],"workflow":{"version":1,"nodes":[],"edges":[]}}
|
package/src/burn-in.js
DELETED
|
@@ -1,91 +0,0 @@
|
|
|
1
|
-
// Metadata burn-in for exported movies, after Blender's render Stamp: a
|
|
2
|
-
// review cut is only reviewable when every frame names itself. The stamp bar
|
|
3
|
-
// carries slate · frame counter and is composited straight into the RGBA
|
|
4
|
-
// bytes the offscreen exporter already produces, so the mp4 pipeline and its
|
|
5
|
-
// determinism guarantees stay untouched.
|
|
6
|
-
//
|
|
7
|
-
// Split on purpose:
|
|
8
|
-
// buildStampText / compositeStamp — pure byte math, unit-testable in node.
|
|
9
|
-
// renderStampBitmap / burnInCapture — need Canvas 2D, exercised in browser QA.
|
|
10
|
-
|
|
11
|
-
/** the slate line for one frame, zero-padded so columns never jitter */
|
|
12
|
-
export function buildStampText({ slate, frame, frameCount }) {
|
|
13
|
-
const digits = Math.max(4, String(Math.max(0, (frameCount ?? 0) - 1)).length);
|
|
14
|
-
const counter = `F ${String(frame).padStart(digits, "0")}`;
|
|
15
|
-
const head = (slate ?? "").trim().toUpperCase();
|
|
16
|
-
return head ? `${head} · ${counter}` : counter;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* Alpha-composite a top-down stamp bitmap into a bottom-up RGBA frame at the
|
|
21
|
-
* bottom-left corner. `pixels` is the raw WebGL readback (row 0 = visual
|
|
22
|
-
* bottom); the stamp is ordinary canvas output (row 0 = visual top). Margins
|
|
23
|
-
* are in pixels of the target frame.
|
|
24
|
-
*/
|
|
25
|
-
export function compositeStamp(pixels, width, height, stamp, { marginX = 0, marginY = 0 } = {}) {
|
|
26
|
-
const { data, width: stampW, height: stampH } = stamp;
|
|
27
|
-
if (!(pixels instanceof Uint8Array) && !(pixels instanceof Uint8ClampedArray)) {
|
|
28
|
-
throw new TypeError("compositeStamp needs raw RGBA bytes");
|
|
29
|
-
}
|
|
30
|
-
if (pixels.byteLength !== width * height * 4) throw new RangeError("frame byte length does not match dimensions");
|
|
31
|
-
if (data.byteLength !== stampW * stampH * 4) throw new RangeError("stamp byte length does not match dimensions");
|
|
32
|
-
const copyW = Math.min(stampW, Math.max(0, width - marginX));
|
|
33
|
-
const copyH = Math.min(stampH, Math.max(0, height - marginY));
|
|
34
|
-
for (let row = 0; row < copyH; row += 1) {
|
|
35
|
-
// stamp row (top-down) → visual y from frame bottom → frame buffer row
|
|
36
|
-
const visualFromBottom = marginY + (copyH - 1 - row);
|
|
37
|
-
const frameRowStart = (visualFromBottom * width + marginX) * 4;
|
|
38
|
-
const stampRowStart = row * stampW * 4;
|
|
39
|
-
for (let col = 0; col < copyW; col += 1) {
|
|
40
|
-
const s = stampRowStart + col * 4;
|
|
41
|
-
const alpha = data[s + 3] / 255;
|
|
42
|
-
if (alpha <= 0) continue;
|
|
43
|
-
const d = frameRowStart + col * 4;
|
|
44
|
-
pixels[d] = Math.round(data[s] * alpha + pixels[d] * (1 - alpha));
|
|
45
|
-
pixels[d + 1] = Math.round(data[s + 1] * alpha + pixels[d + 1] * (1 - alpha));
|
|
46
|
-
pixels[d + 2] = Math.round(data[s + 2] * alpha + pixels[d + 2] * (1 - alpha));
|
|
47
|
-
pixels[d + 3] = 255;
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
return pixels;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
/** Rasterize one stamp line into RGBA bytes (browser-only: Canvas 2D). */
|
|
54
|
-
function renderStampBitmap(text, { fontPx = 18, padX = 10, padY = 6, maxWidth = Infinity } = {}) {
|
|
55
|
-
const probe = new OffscreenCanvas(1, 1).getContext("2d");
|
|
56
|
-
probe.font = `600 ${fontPx}px ui-monospace, Menlo, monospace`;
|
|
57
|
-
const textW = Math.ceil(probe.measureText(text).width);
|
|
58
|
-
const width = Math.max(1, Math.min(textW + padX * 2, Math.floor(maxWidth)));
|
|
59
|
-
const height = fontPx + padY * 2;
|
|
60
|
-
const canvas = new OffscreenCanvas(width, height);
|
|
61
|
-
const ctx = canvas.getContext("2d", { willReadFrequently: true });
|
|
62
|
-
ctx.fillStyle = "rgba(14, 13, 16, 0.72)";
|
|
63
|
-
ctx.fillRect(0, 0, width, height);
|
|
64
|
-
ctx.font = probe.font;
|
|
65
|
-
ctx.textBaseline = "middle";
|
|
66
|
-
ctx.fillStyle = "rgba(255, 255, 255, 0.94)";
|
|
67
|
-
ctx.fillText(text, padX, height / 2 + 1, width - padX * 2);
|
|
68
|
-
const image = ctx.getImageData(0, 0, width, height);
|
|
69
|
-
return { data: image.data, width, height };
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
/**
|
|
73
|
-
* Wrap an exporter `capture(frame)` so every returned frame carries the burn-in
|
|
74
|
-
* bar. Bitmaps are cached per rendered text, so the per-frame cost is one
|
|
75
|
-
* composite pass; only the frame counter changes between frames.
|
|
76
|
-
*/
|
|
77
|
-
export function burnInCapture(capture, { width, height, slate, frameCount }) {
|
|
78
|
-
const cache = new Map();
|
|
79
|
-
const margin = Math.max(8, Math.round(height * 0.02));
|
|
80
|
-
return (frame) => {
|
|
81
|
-
const pixels = capture(frame);
|
|
82
|
-
if (!pixels) return pixels;
|
|
83
|
-
const text = buildStampText({ slate, frame, frameCount });
|
|
84
|
-
let bitmap = cache.get(text);
|
|
85
|
-
if (!bitmap) {
|
|
86
|
-
bitmap = renderStampBitmap(text, { fontPx: Math.max(12, Math.round(height / 40)), maxWidth: width - margin * 2 });
|
|
87
|
-
cache.set(text, bitmap);
|
|
88
|
-
}
|
|
89
|
-
return compositeStamp(pixels, width, height, bitmap, { marginX: margin, marginY: margin });
|
|
90
|
-
};
|
|
91
|
-
}
|
package/src/camera-tutorial.jsx
DELETED
|
@@ -1,520 +0,0 @@
|
|
|
1
|
-
import { useEffect, useMemo, useRef, useState } from "react";
|
|
2
|
-
import { createPortal } from "react-dom";
|
|
3
|
-
import { ko } from "./locale.js";
|
|
4
|
-
|
|
5
|
-
// The camera tutorial, Studio-native (#206).
|
|
6
|
-
//
|
|
7
|
-
// The landing page runs the same seven steps against the playground iframe
|
|
8
|
-
// (index.html), where the studio is a black box and every signal crosses the
|
|
9
|
-
// frame boundary as a postMessage. Inside /app/ there is no frame: the
|
|
10
|
-
// gestures already announce themselves on `window`, so this component listens
|
|
11
|
-
// to them directly.
|
|
12
|
-
//
|
|
13
|
-
// cozyclay:nav fly / walk / dolly / orbit (src/controls.jsx)
|
|
14
|
-
// shotCount / railReady shot / rail (authored state)
|
|
15
|
-
// previewing prop play (lookThroughShot)
|
|
16
|
-
//
|
|
17
|
-
// Nothing here drives the studio. A step is done when the operator has done
|
|
18
|
-
// the thing in the real tool — there is no "next" button to fake progress
|
|
19
|
-
// with, which is the whole point of teaching a camera by flying it.
|
|
20
|
-
|
|
21
|
-
/** Right-button + these six keys is one gesture, so the walk step only counts
|
|
22
|
-
* once every key has actually been pressed (the landing page's rule). */
|
|
23
|
-
export const WALK_KEYS = ["w", "a", "s", "d", "q", "e"];
|
|
24
|
-
|
|
25
|
-
export const CAMERA_TUTORIAL_STEPS = [
|
|
26
|
-
{
|
|
27
|
-
kind: "fly",
|
|
28
|
-
label: ko("Look", "보기"),
|
|
29
|
-
how: () => ko(
|
|
30
|
-
"Right-drag in the viewport to look around.",
|
|
31
|
-
"뷰포트에서 오른쪽 버튼을 끌어 주위를 둘러보세요.",
|
|
32
|
-
),
|
|
33
|
-
},
|
|
34
|
-
{
|
|
35
|
-
kind: "walk",
|
|
36
|
-
label: ko("Walk", "걷기"),
|
|
37
|
-
how: ({ walked }) => {
|
|
38
|
-
const keys = (
|
|
39
|
-
<span className="camera-tutorial-keys">
|
|
40
|
-
{WALK_KEYS.map((key) => (
|
|
41
|
-
<kbd key={key} data-key={key} data-done={walked.has(key) ? 1 : 0}>{key.toUpperCase()}</kbd>
|
|
42
|
-
))}
|
|
43
|
-
</span>
|
|
44
|
-
);
|
|
45
|
-
return ko(
|
|
46
|
-
<>Hold the right button and press each key once: {keys} W A S D walk, Q E crane.</>,
|
|
47
|
-
<>오른쪽 버튼을 누른 채 각 키를 한 번씩 누르세요: {keys} W A S D 이동, Q E 크레인.</>,
|
|
48
|
-
);
|
|
49
|
-
},
|
|
50
|
-
},
|
|
51
|
-
{
|
|
52
|
-
kind: "dolly",
|
|
53
|
-
label: ko("Dolly", "돌리"),
|
|
54
|
-
how: () => ko(
|
|
55
|
-
"Scroll in the viewport to push in and pull out.",
|
|
56
|
-
"뷰포트에서 스크롤해 앞뒤로 밀고 당겨 보세요.",
|
|
57
|
-
),
|
|
58
|
-
},
|
|
59
|
-
{
|
|
60
|
-
kind: "orbit",
|
|
61
|
-
label: ko("Orbit", "궤도"),
|
|
62
|
-
how: () => ko(
|
|
63
|
-
<>Hold <kbd>Alt</kbd> (<kbd>⌥ Option</kbd> on Mac) and left-drag to circle the character.</>,
|
|
64
|
-
<><kbd>Alt</kbd>(맥은 <kbd>⌥ Option</kbd>)을 누른 채 왼쪽 버튼을 끌어 캐릭터 주위를 도세요.</>,
|
|
65
|
-
),
|
|
66
|
-
},
|
|
67
|
-
{
|
|
68
|
-
kind: "shot",
|
|
69
|
-
label: ko("Shot", "샷"),
|
|
70
|
-
// The control lives in another region than the card, so the card says
|
|
71
|
-
// which way to look before it says what to do.
|
|
72
|
-
where: ko("↓ Timeline, Camera Cuts", "↓ 타임라인 Camera Cuts"),
|
|
73
|
-
how: () => ko(
|
|
74
|
-
<>In the timeline's Camera Cuts lane click <b>+ Add shot</b>. That is your cut.</>,
|
|
75
|
-
<>타임라인의 Camera Cuts 레인에서 <b>+ 샷 추가</b>를 누르세요. 그게 컷입니다.</>,
|
|
76
|
-
),
|
|
77
|
-
},
|
|
78
|
-
{
|
|
79
|
-
kind: "rail",
|
|
80
|
-
label: ko("Rail", "레일"),
|
|
81
|
-
where: ko("↓ Timeline, Camera Cuts", "↓ 타임라인 Camera Cuts"),
|
|
82
|
-
how: () => ko(
|
|
83
|
-
<>Select the shot, click <b>Draw rail</b> in the camera bar, and drag a line across the top view. That line is the dolly move.</>,
|
|
84
|
-
<>샷을 선택하고 카메라 바의 <b>레일 그리기</b>를 누른 뒤, 탑뷰에 선을 그으세요. 그 선이 돌리 이동입니다.</>,
|
|
85
|
-
),
|
|
86
|
-
},
|
|
87
|
-
{
|
|
88
|
-
kind: "play",
|
|
89
|
-
label: ko("Play", "재생"),
|
|
90
|
-
where: ko("↘ Shot Camera monitor", "↘ Shot Camera 모니터"),
|
|
91
|
-
how: () => ko(
|
|
92
|
-
<>Click <b>Look through</b> in the Shot monitor to fly the shot camera; <b>▶</b> rides the rail, <kbd>Esc</kbd> returns to the free camera.</>,
|
|
93
|
-
<>Shot Camera 모니터의 <b>샷 시점</b>을 눌러 샷 카메라를 조종하세요. <b>▶</b>는 레일을 타고, <kbd>Esc</kbd>로 자유 카메라로 돌아옵니다.</>,
|
|
94
|
-
),
|
|
95
|
-
},
|
|
96
|
-
];
|
|
97
|
-
|
|
98
|
-
const NAV_KINDS = new Set(["fly", "walk", "dolly", "orbit"]);
|
|
99
|
-
const SIGNAL_KINDS = new Set(["shot", "rail"]);
|
|
100
|
-
|
|
101
|
-
// Where each of the last three steps actually happens (#211). The card names
|
|
102
|
-
// the gesture; these pin a numbered circle and a caption onto the control
|
|
103
|
-
// itself, so "click + Add shot" stops being a treasure hunt across two panes.
|
|
104
|
-
//
|
|
105
|
-
// `absent`/`requires` gate a beacon on the studio's own state instead of on
|
|
106
|
-
// tutorial bookkeeping: the camera bar (.tl-rail-draw) only exists once a shot
|
|
107
|
-
// is selected, which is exactly the moment the rail step's advice changes.
|
|
108
|
-
export const TUTORIAL_BEACONS = {
|
|
109
|
-
shot: [
|
|
110
|
-
{ role: "add-shot", selector: ".tl-track.shots .tl-track-add", label: () => ko("Click here", "여기를 클릭") },
|
|
111
|
-
],
|
|
112
|
-
rail: [
|
|
113
|
-
{
|
|
114
|
-
role: "select-shot",
|
|
115
|
-
selector: ".tl-track.shots .tl-shot-block:not(.selected)",
|
|
116
|
-
absent: ".tl-rail-draw",
|
|
117
|
-
label: () => ko("1. Select the shot", "1. 샷을 선택"),
|
|
118
|
-
},
|
|
119
|
-
{ role: "draw-rail", selector: ".tl-rail-draw", label: () => ko("2. Draw rail, then drag across the top view", "2. 레일 그리기 후 탑뷰에 선 긋기") },
|
|
120
|
-
// The Top-View becomes an active drawing surface after the tool is armed.
|
|
121
|
-
{ role: "top-view", selector: ".vp-inset", requires: '[data-rail-draw="1"]', label: () => ko("Drag a line here", "여기에 선을 그으세요") },
|
|
122
|
-
],
|
|
123
|
-
play: [
|
|
124
|
-
// The glass shell keeps the same action in the bottom dock while the
|
|
125
|
-
// compact viewport card owns it in the classic shell.
|
|
126
|
-
{ role: "look-through", selector: ".dock-shot-look, .vp-look-through", label: () => ko("Click to fly the shot camera", "클릭해 샷 카메라를 조종") },
|
|
127
|
-
],
|
|
128
|
-
};
|
|
129
|
-
|
|
130
|
-
/** the beacon's own geometry, in px — the circle sits above-left of the target */
|
|
131
|
-
const DOT = 22;
|
|
132
|
-
const LIFT_X = 30;
|
|
133
|
-
const LIFT_Y = 44;
|
|
134
|
-
/** nominal box used only for edge clamping; the chip itself is auto-sized */
|
|
135
|
-
const BEACON_W = 190;
|
|
136
|
-
const BEACON_H = 26;
|
|
137
|
-
|
|
138
|
-
const reducedMotion = () => globalThis.matchMedia?.("(prefers-reduced-motion: reduce)").matches === true;
|
|
139
|
-
|
|
140
|
-
const boxOf = (element) => {
|
|
141
|
-
if (!element) return null;
|
|
142
|
-
const box = element.getBoundingClientRect();
|
|
143
|
-
if (box.width < 2 || box.height < 2) return null;
|
|
144
|
-
if (box.right < 0 || box.bottom < 0 || box.left > window.innerWidth || box.top > window.innerHeight) return null;
|
|
145
|
-
return box;
|
|
146
|
-
};
|
|
147
|
-
|
|
148
|
-
/** first element matching `selector` that is actually laid out on screen */
|
|
149
|
-
const firstVisible = (selector) => {
|
|
150
|
-
if (!selector) return null;
|
|
151
|
-
for (const element of document.querySelectorAll(selector)) {
|
|
152
|
-
const box = boxOf(element);
|
|
153
|
-
if (box) return { element, box };
|
|
154
|
-
}
|
|
155
|
-
return null;
|
|
156
|
-
};
|
|
157
|
-
|
|
158
|
-
const round = (value) => Math.round(value * 2) / 2;
|
|
159
|
-
|
|
160
|
-
/**
|
|
161
|
-
* Place the beacon above-left of the target and clamp it inside whatever
|
|
162
|
-
* scrolls the target (the timeline body, or the viewport pane). The leader
|
|
163
|
-
* line is aimed at the real target, so a shot block scrolled past the lane's
|
|
164
|
-
* edge still gets an arrow pointing off toward it.
|
|
165
|
-
*
|
|
166
|
-
* Above is the default; when the target hugs the top of its own pane — the
|
|
167
|
-
* camera bar under the transport row, the Top-View inset under the titlebar —
|
|
168
|
-
* the beacon flips below rather than sitting on the pane's header controls.
|
|
169
|
-
*/
|
|
170
|
-
function place(element, box) {
|
|
171
|
-
const clip = element.closest(".tl-body, .timeline, .viewport")?.getBoundingClientRect();
|
|
172
|
-
const edge = Math.min(14, box.height / 2);
|
|
173
|
-
const above = box.top + edge - LIFT_Y - DOT / 2;
|
|
174
|
-
const flip = above < (clip ? clip.top : 0) + BEACON_H;
|
|
175
|
-
const aimX = box.left + Math.min(18, box.width / 2);
|
|
176
|
-
const aimY = flip ? box.bottom - edge : box.top + edge;
|
|
177
|
-
const minLeft = Math.max(8, clip ? clip.left + 6 : 8);
|
|
178
|
-
const maxLeft = Math.max(minLeft, Math.min(window.innerWidth - 8, clip ? clip.right - 6 : window.innerWidth - 8) - BEACON_W);
|
|
179
|
-
const left = Math.min(Math.max(aimX - LIFT_X - DOT / 2, minLeft), maxLeft);
|
|
180
|
-
const top = Math.min(Math.max(flip ? aimY + LIFT_Y - DOT / 2 : above, 8), Math.max(8, window.innerHeight - 46));
|
|
181
|
-
return { left: round(left), top: round(top), aimX: round(aimX - left), aimY: round(aimY - top) };
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
const samePlace = (a, b) => !!a && !!b && a.left === b.left && a.top === b.top && a.aimX === b.aimX && a.aimY === b.aimY;
|
|
185
|
-
|
|
186
|
-
/**
|
|
187
|
-
* One numbered circle plus a caption chip, portalled to <body> and pinned to a
|
|
188
|
-
* live control. It measures on mount, on resize, on scroll, on a
|
|
189
|
-
* ResizeObserver of the timeline and viewport, and on any DOM mutation — the
|
|
190
|
-
* camera bar mounts only after a shot is selected, so "target not there yet"
|
|
191
|
-
* is the normal case, not an error. Pointer-transparent: the control it points
|
|
192
|
-
* at must stay clickable through it.
|
|
193
|
-
*/
|
|
194
|
-
export function TutorialBeacon({ kind, step, role, selector, label, requires = null, absent = null }) {
|
|
195
|
-
const [spot, setSpot] = useState(null);
|
|
196
|
-
|
|
197
|
-
useEffect(() => {
|
|
198
|
-
let frame = 0;
|
|
199
|
-
const measure = () => {
|
|
200
|
-
frame = 0;
|
|
201
|
-
if (absent && firstVisible(absent)) return setSpot(null);
|
|
202
|
-
if (requires && !firstVisible(requires)) return setSpot(null);
|
|
203
|
-
const found = firstVisible(selector);
|
|
204
|
-
if (!found) return setSpot(null);
|
|
205
|
-
const next = place(found.element, found.box);
|
|
206
|
-
// Same numbers means no re-render, which keeps the MutationObserver
|
|
207
|
-
// below from chasing this component's own portal forever.
|
|
208
|
-
setSpot((prev) => (samePlace(prev, next) ? prev : next));
|
|
209
|
-
};
|
|
210
|
-
const schedule = () => {
|
|
211
|
-
if (!frame) frame = requestAnimationFrame(measure);
|
|
212
|
-
};
|
|
213
|
-
measure();
|
|
214
|
-
window.addEventListener("resize", schedule);
|
|
215
|
-
window.addEventListener("scroll", schedule, true);
|
|
216
|
-
const resize = new ResizeObserver(schedule);
|
|
217
|
-
for (const pane of [document.querySelector(".timeline"), document.querySelector(".viewport")]) {
|
|
218
|
-
if (pane) resize.observe(pane);
|
|
219
|
-
}
|
|
220
|
-
const mutations = new MutationObserver(schedule);
|
|
221
|
-
mutations.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ["class", "style", "data-rail-draw"] });
|
|
222
|
-
return () => {
|
|
223
|
-
if (frame) cancelAnimationFrame(frame);
|
|
224
|
-
window.removeEventListener("resize", schedule);
|
|
225
|
-
window.removeEventListener("scroll", schedule, true);
|
|
226
|
-
resize.disconnect();
|
|
227
|
-
mutations.disconnect();
|
|
228
|
-
};
|
|
229
|
-
}, [selector, requires, absent]);
|
|
230
|
-
|
|
231
|
-
if (!spot) return null;
|
|
232
|
-
return createPortal(
|
|
233
|
-
<div
|
|
234
|
-
className="tutorial-beacon"
|
|
235
|
-
data-testid="camera-tutorial-beacon"
|
|
236
|
-
data-kind={kind}
|
|
237
|
-
data-role={role}
|
|
238
|
-
style={{ left: `${spot.left}px`, top: `${spot.top}px` }}
|
|
239
|
-
>
|
|
240
|
-
<svg className="tutorial-beacon-leader" aria-hidden="true">
|
|
241
|
-
<line x1={DOT / 2} y1={DOT / 2} x2={spot.aimX} y2={spot.aimY} />
|
|
242
|
-
</svg>
|
|
243
|
-
<span className="tutorial-beacon-dot" aria-hidden="true">{step}</span>
|
|
244
|
-
<span className="tutorial-beacon-label">{label()}</span>
|
|
245
|
-
</div>,
|
|
246
|
-
document.body,
|
|
247
|
-
);
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
// Steps 1–4 have no control to point at: the gesture IS the interface. The cue
|
|
251
|
-
// draws the mouse and the keys the step wants, animated the way the hand is
|
|
252
|
-
// supposed to move, and sits centre-bottom of the viewport where the eye
|
|
253
|
-
// already is while flying.
|
|
254
|
-
const GESTURE_CAPTIONS = {
|
|
255
|
-
fly: () => ko("Right-drag to look", "오른쪽 끌어 둘러보기"),
|
|
256
|
-
walk: () => ko("Hold right, press keys", "오른쪽 누른 채 키 입력"),
|
|
257
|
-
dolly: () => ko("Scroll to push in", "스크롤로 밀고 당기기"),
|
|
258
|
-
orbit: () => ko("Alt + left-drag to circle", "Alt+왼쪽 끌어 돌기"),
|
|
259
|
-
};
|
|
260
|
-
|
|
261
|
-
function MouseGlyph() {
|
|
262
|
-
return (
|
|
263
|
-
<svg className="tutorial-cue-mouse" viewBox="0 0 40 58" aria-hidden="true">
|
|
264
|
-
<rect className="cue-mouse-body" x="4" y="3" width="32" height="52" rx="16" />
|
|
265
|
-
<path className="cue-mouse-left" d="M20 3 A16 16 0 0 0 4 19 L20 19 Z" />
|
|
266
|
-
<path className="cue-mouse-right" d="M20 3 A16 16 0 0 1 36 19 L20 19 Z" />
|
|
267
|
-
<rect className="cue-mouse-wheel" x="17.5" y="8" width="5" height="11" rx="2.5" />
|
|
268
|
-
</svg>
|
|
269
|
-
);
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
/**
|
|
273
|
-
* The animated gesture glyph for one nav step. `leaving` starts the fade; the
|
|
274
|
-
* parent unmounts it when the opacity transition ends, so the cue never blinks
|
|
275
|
-
* out mid-step. Under prefers-reduced-motion the glyph is static (styles.css)
|
|
276
|
-
* and the parent drops it outright.
|
|
277
|
-
*/
|
|
278
|
-
export function GestureCue({ kind, walked, leaving = false, onLeft }) {
|
|
279
|
-
const caption = GESTURE_CAPTIONS[kind]?.() ?? "";
|
|
280
|
-
return (
|
|
281
|
-
<div
|
|
282
|
-
className="tutorial-cue"
|
|
283
|
-
data-testid="camera-tutorial-gesture"
|
|
284
|
-
data-kind={kind}
|
|
285
|
-
data-leaving={leaving ? 1 : 0}
|
|
286
|
-
aria-hidden="true"
|
|
287
|
-
onTransitionEnd={(event) => {
|
|
288
|
-
if (leaving && event.propertyName === "opacity" && event.target === event.currentTarget) onLeft?.();
|
|
289
|
-
}}
|
|
290
|
-
>
|
|
291
|
-
<div className="tutorial-cue-stage">
|
|
292
|
-
{kind === "orbit" && <span className="tutorial-cue-key">Alt</span>}
|
|
293
|
-
<span className="tutorial-cue-arm">
|
|
294
|
-
<MouseGlyph />
|
|
295
|
-
{kind === "fly" && (
|
|
296
|
-
<svg className="tutorial-cue-arc" viewBox="0 0 64 20" aria-hidden="true">
|
|
297
|
-
<path className="cue-arc-line" d="M8 14 Q32 3 56 14" />
|
|
298
|
-
<path className="cue-arc-head" d="M13 9 L7 14 L14 17" />
|
|
299
|
-
<path className="cue-arc-head" d="M51 9 L57 14 L50 17" />
|
|
300
|
-
</svg>
|
|
301
|
-
)}
|
|
302
|
-
</span>
|
|
303
|
-
{kind === "orbit" && <span className="tutorial-cue-pivot" />}
|
|
304
|
-
{kind === "dolly" && (
|
|
305
|
-
<svg className="tutorial-cue-updown" viewBox="0 0 14 44" aria-hidden="true">
|
|
306
|
-
<path className="cue-up" d="M7 16 L7 4 M3 8 L7 4 L11 8" />
|
|
307
|
-
<path className="cue-down" d="M7 28 L7 40 M3 36 L7 40 L11 36" />
|
|
308
|
-
</svg>
|
|
309
|
-
)}
|
|
310
|
-
</div>
|
|
311
|
-
{kind === "walk" && (
|
|
312
|
-
<span className="tutorial-cue-keys">
|
|
313
|
-
{WALK_KEYS.map((walkKey) => (
|
|
314
|
-
<kbd key={walkKey} data-done={walked?.has(walkKey) ? 1 : 0}>{walkKey.toUpperCase()}</kbd>
|
|
315
|
-
))}
|
|
316
|
-
</span>
|
|
317
|
-
)}
|
|
318
|
-
<span className="tutorial-cue-caption">{caption}</span>
|
|
319
|
-
</div>
|
|
320
|
-
);
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
/**
|
|
324
|
-
* The overlay itself: a seven-chip strip plus one hint card for the step the
|
|
325
|
-
* operator is on. It sits at the top of the viewport pane, left of the
|
|
326
|
-
* Top-View inset, and never takes the pointer except on its own close button
|
|
327
|
-
* — every step is completed by working the studio underneath it.
|
|
328
|
-
*/
|
|
329
|
-
export function CameraTutorial({ previsMode, previewing = false, shotCount = 0, railReady = false, analytics, onStepChange, onClose, onComplete, handoff = null, shotId = null, onOpenExport, onContinue }) {
|
|
330
|
-
const [done, setDone] = useState(() => new Set());
|
|
331
|
-
const [walked, setWalked] = useState(() => new Set());
|
|
332
|
-
const [cue, setCue] = useState(null);
|
|
333
|
-
const completionReported = useRef(false);
|
|
334
|
-
const effectiveDone = useMemo(() => {
|
|
335
|
-
const next = new Set(done);
|
|
336
|
-
if (shotCount > 0) next.add("shot");
|
|
337
|
-
if (railReady) next.add("rail");
|
|
338
|
-
if (previewing && next.has("rail")) next.add("play");
|
|
339
|
-
return next;
|
|
340
|
-
}, [done, previewing, railReady, shotCount]);
|
|
341
|
-
|
|
342
|
-
useEffect(() => {
|
|
343
|
-
const complete = (kind) => setDone((current) => (current.has(kind) ? current : new Set(current).add(kind)));
|
|
344
|
-
const onNav = (event) => {
|
|
345
|
-
const kind = event.detail?.kind;
|
|
346
|
-
if (!NAV_KINDS.has(kind)) return;
|
|
347
|
-
if (kind !== "walk") {
|
|
348
|
-
complete(kind);
|
|
349
|
-
return;
|
|
350
|
-
}
|
|
351
|
-
const key = typeof event.detail?.key === "string" ? event.detail.key.toLowerCase() : null;
|
|
352
|
-
if (!WALK_KEYS.includes(key)) return;
|
|
353
|
-
setWalked((current) => {
|
|
354
|
-
if (current.has(key)) return current;
|
|
355
|
-
const next = new Set(current).add(key);
|
|
356
|
-
if (WALK_KEYS.every((walkKey) => next.has(walkKey))) complete("walk");
|
|
357
|
-
return next;
|
|
358
|
-
});
|
|
359
|
-
};
|
|
360
|
-
const onSignal = (event) => {
|
|
361
|
-
if (SIGNAL_KINDS.has(event.detail?.kind)) complete(event.detail.kind);
|
|
362
|
-
};
|
|
363
|
-
window.addEventListener("cozyclay:nav", onNav);
|
|
364
|
-
window.addEventListener("cozyclay:playground-signal", onSignal);
|
|
365
|
-
return () => {
|
|
366
|
-
window.removeEventListener("cozyclay:nav", onNav);
|
|
367
|
-
window.removeEventListener("cozyclay:playground-signal", onSignal);
|
|
368
|
-
};
|
|
369
|
-
}, []);
|
|
370
|
-
|
|
371
|
-
// Studio-native completion comes from the authored state. The old playground
|
|
372
|
-
// signal was never emitted by the Studio shell, so a real Shot or rail could
|
|
373
|
-
// leave the tutorial stuck on its previous step.
|
|
374
|
-
useEffect(() => {
|
|
375
|
-
if (shotCount > 0) setDone((current) => (current.has("shot") ? current : new Set(current).add("shot")));
|
|
376
|
-
}, [shotCount]);
|
|
377
|
-
useEffect(() => {
|
|
378
|
-
if (railReady) setDone((current) => (current.has("rail") ? current : new Set(current).add("rail")));
|
|
379
|
-
}, [railReady]);
|
|
380
|
-
|
|
381
|
-
// The player is the last step, and only once there is a rail to ride:
|
|
382
|
-
// look-through before the dolly exists shows a still frame, which teaches
|
|
383
|
-
// nothing about the move. Analytics v1 observes this existing look-through
|
|
384
|
-
// boundary, not transport playback or reaching the end of a shot.
|
|
385
|
-
useEffect(() => {
|
|
386
|
-
if (!previewing) return;
|
|
387
|
-
setDone((current) => (!current.has("rail") || current.has("play") ? current : new Set(current).add("play")));
|
|
388
|
-
}, [previewing, railReady]);
|
|
389
|
-
|
|
390
|
-
const current = useMemo(() => CAMERA_TUTORIAL_STEPS.find((step) => !effectiveDone.has(step.kind)) ?? null, [effectiveDone]);
|
|
391
|
-
const complete = current === null;
|
|
392
|
-
const currentKind = current?.kind ?? null;
|
|
393
|
-
const showHandoff = !!shotId && (handoff?.canShow(effectiveDone) ?? false);
|
|
394
|
-
useEffect(() => {
|
|
395
|
-
if (!complete || completionReported.current) return;
|
|
396
|
-
completionReported.current = true;
|
|
397
|
-
onComplete?.();
|
|
398
|
-
}, [complete, onComplete]);
|
|
399
|
-
|
|
400
|
-
// Observe committed progression, outside state updaters (which StrictMode
|
|
401
|
-
// may replay). The attempt belongs to App and survives effect cleanup/resume.
|
|
402
|
-
useEffect(() => {
|
|
403
|
-
analytics?.observe(effectiveDone, currentKind);
|
|
404
|
-
}, [analytics, effectiveDone, currentKind]);
|
|
405
|
-
|
|
406
|
-
// The step the operator is on is the studio's business too: App puts it on
|
|
407
|
-
// the .app root as data-tutorial-step, and styles.css spotlights whichever
|
|
408
|
-
// control that step needs (#211).
|
|
409
|
-
useEffect(() => {
|
|
410
|
-
onStepChange?.(currentKind);
|
|
411
|
-
}, [currentKind, onStepChange]);
|
|
412
|
-
useEffect(() => () => onStepChange?.(null), [onStepChange]);
|
|
413
|
-
|
|
414
|
-
// Steps 1–4 carry a gesture cue instead of a beacon. It fades out when its
|
|
415
|
-
// step completes rather than vanishing under the hand that just finished it.
|
|
416
|
-
useEffect(() => {
|
|
417
|
-
if (currentKind && NAV_KINDS.has(currentKind)) {
|
|
418
|
-
setCue({ kind: currentKind, leaving: false });
|
|
419
|
-
return;
|
|
420
|
-
}
|
|
421
|
-
setCue((prev) => {
|
|
422
|
-
if (!prev) return prev;
|
|
423
|
-
if (reducedMotion()) return null;
|
|
424
|
-
return prev.leaving ? prev : { ...prev, leaving: true };
|
|
425
|
-
});
|
|
426
|
-
}, [currentKind]);
|
|
427
|
-
|
|
428
|
-
// Storyboard projects never show the camera tutorial (its steps are shot/rail/play).
|
|
429
|
-
if (previsMode === "storyboard") return null;
|
|
430
|
-
|
|
431
|
-
return (
|
|
432
|
-
<>
|
|
433
|
-
<aside
|
|
434
|
-
className="camera-tutorial"
|
|
435
|
-
data-testid="camera-tutorial"
|
|
436
|
-
data-state={complete ? "done" : "active"}
|
|
437
|
-
data-previewing={previewing ? 1 : 0}
|
|
438
|
-
data-handoff={showHandoff ? 1 : undefined}
|
|
439
|
-
aria-label={ko("Camera tutorial", "카메라 튜토리얼")}
|
|
440
|
-
>
|
|
441
|
-
<ol className="camera-tutorial-steps">
|
|
442
|
-
{CAMERA_TUTORIAL_STEPS.map((step, index) => (
|
|
443
|
-
<li
|
|
444
|
-
key={step.kind}
|
|
445
|
-
data-testid="camera-tutorial-step"
|
|
446
|
-
data-kind={step.kind}
|
|
447
|
-
data-done={effectiveDone.has(step.kind) ? 1 : 0}
|
|
448
|
-
data-current={step === current ? 1 : 0}
|
|
449
|
-
>
|
|
450
|
-
<i aria-hidden="true">{effectiveDone.has(step.kind) ? "✓" : index + 1}</i>
|
|
451
|
-
{step.label}
|
|
452
|
-
</li>
|
|
453
|
-
))}
|
|
454
|
-
</ol>
|
|
455
|
-
<div className="camera-tutorial-card" data-testid="camera-tutorial-card" role="status">
|
|
456
|
-
{complete ? (
|
|
457
|
-
<>
|
|
458
|
-
<span className="camera-tutorial-count done">{ko("Done", "완료")}</span>
|
|
459
|
-
<p>{ko(
|
|
460
|
-
"That is the whole camera: look, walk, dolly, orbit, cut, rail, play.",
|
|
461
|
-
"카메라의 전부입니다: 보기, 걷기, 돌리, 궤도, 컷, 레일, 재생.",
|
|
462
|
-
)}</p>
|
|
463
|
-
</>
|
|
464
|
-
) : (
|
|
465
|
-
<>
|
|
466
|
-
<span className="camera-tutorial-count">{`${CAMERA_TUTORIAL_STEPS.indexOf(current) + 1} / ${CAMERA_TUTORIAL_STEPS.length}`}</span>
|
|
467
|
-
<p>
|
|
468
|
-
{current.where && <span className="camera-tutorial-where">{current.where}</span>}
|
|
469
|
-
{current.how({ walked })}
|
|
470
|
-
</p>
|
|
471
|
-
</>
|
|
472
|
-
)}
|
|
473
|
-
</div>
|
|
474
|
-
{showHandoff && (
|
|
475
|
-
<div className="camera-tutorial-handoff" data-testid="camera-tutorial-handoff">
|
|
476
|
-
<p>{ko("Take this shot with you. Its camera and range stay yours.", "이 샷을 가져가세요. 설정한 카메라와 범위가 그대로 유지됩니다.")}</p>
|
|
477
|
-
<button type="button" className="btn primary" data-testid="camera-tutorial-handoff-export" onClick={() => onOpenExport?.(shotId)}>
|
|
478
|
-
{ko("Open Export", "내보내기 열기")}
|
|
479
|
-
</button>
|
|
480
|
-
<button type="button" className="camera-tutorial-handoff-dismiss" data-testid="camera-tutorial-handoff-dismiss" onClick={() => onContinue?.()}>
|
|
481
|
-
{ko("Continue editing", "편집 계속하기")}
|
|
482
|
-
</button>
|
|
483
|
-
</div>
|
|
484
|
-
)}
|
|
485
|
-
<button
|
|
486
|
-
type="button"
|
|
487
|
-
className="camera-tutorial-close"
|
|
488
|
-
data-testid="camera-tutorial-close"
|
|
489
|
-
aria-label={ko("Close tutorial", "튜토리얼 닫기")}
|
|
490
|
-
title={ko("Close tutorial", "튜토리얼 닫기")}
|
|
491
|
-
onClick={() => { analytics?.dismiss(); onClose?.(); }}
|
|
492
|
-
>
|
|
493
|
-
×
|
|
494
|
-
</button>
|
|
495
|
-
</aside>
|
|
496
|
-
{cue && (
|
|
497
|
-
<GestureCue
|
|
498
|
-
kind={cue.kind}
|
|
499
|
-
walked={walked}
|
|
500
|
-
leaving={cue.leaving}
|
|
501
|
-
onLeft={() => setCue((prev) => (prev?.leaving ? null : prev))}
|
|
502
|
-
/>
|
|
503
|
-
)}
|
|
504
|
-
{(TUTORIAL_BEACONS[currentKind] ?? []).map((beacon) => (
|
|
505
|
-
<TutorialBeacon
|
|
506
|
-
key={beacon.role}
|
|
507
|
-
kind={currentKind}
|
|
508
|
-
step={CAMERA_TUTORIAL_STEPS.indexOf(current) + 1}
|
|
509
|
-
role={beacon.role}
|
|
510
|
-
selector={beacon.selector}
|
|
511
|
-
label={beacon.label}
|
|
512
|
-
requires={beacon.requires}
|
|
513
|
-
absent={beacon.absent}
|
|
514
|
-
/>
|
|
515
|
-
))}
|
|
516
|
-
</>
|
|
517
|
-
);
|
|
518
|
-
}
|
|
519
|
-
|
|
520
|
-
export default CameraTutorial;
|