如何通过Stimulus控制器将Active Storage图片加载到Canvas?
如何在Stimulus控制器中加载Active Storage图片到Three.js
问题背景
技术栈为Rails、esbuild/jsbundling-rails、Active Storage、Stimulus JS、Three.js。现有User和Photo模型,Photo模型配置了has_one_attached :image,当前在index页面用Stimulus控制器渲染Three.js场景,但使用的是本地打包图片,需要替换为Active Storage中的单张图片。
解决步骤
1. 将Active Storage图片URL传入Stimulus控制器
在index.html.erb中,修改canvas标签的data属性,把目标图片的Active Storage URL通过Stimulus的value语法传递给控制器。这里以获取第一个Photo实例的图片为例:
<%= content_tag(:canvas, "", data: { controller: "threejs", threejs_image_url_value: rails_blob_url(Photo.first.image) }) %>
如果需要指定特定Photo,只需把Photo.first替换为对应的实例即可。
2. 修改Stimulus控制器,使用传入的图片URL
打开你的threejs Stimulus控制器,做以下修改:
- 移除本地图片的导入语句:
import TentImage from "../images/tent.png"; - 添加Stimulus value定义,用来接收传入的图片URL
- 在
loadTexture方法中使用传入的URL替换原来的本地图片路径
修改后的控制器核心代码如下:
import { Controller } from "@hotwired/stimulus"; import * as THREE from "three"; import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; import GUI from 'lil-gui'; // Connects to data-controller="threejs" export default class extends Controller { // 定义接收图片URL的value static values = { imageUrl: String } connect() { this.initThreeJS(); this.loadTexture(); } loadTexture() { const textureLoader = new THREE.TextureLoader(); // 使用传入的Active Storage图片URL textureLoader.load(this.imageUrlValue, (texture) => { this.texture = texture; this.rectangles.forEach((rectangle) => { rectangle.material.map = this.texture; rectangle.material.needsUpdate = true; }); console.log("Texture loaded!", texture); }); } // 其他Three.js初始化代码保持不变... initThreeJS() { /** Debug */ this.gui = new GUI(); /** Scene, Camera, Renderer */ this.scene = new THREE.Scene(); this.camera = new THREE.PerspectiveCamera( 75, window.innerWidth / window.innerHeight, 0.1, 1000 ); this.renderer = new THREE.WebGLRenderer(); this.renderer.setClearColor(0xFFFFFF); this.sizes = { width: window.innerWidth, height: window.innerHeight } this.renderer.setSize( this.sizes.width, this.sizes.height ); this.renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)); document.body.appendChild(this.renderer.domElement); /** Circle */ const circleGeometry = new THREE.CircleGeometry( 2, 32 ); this.wireframeMaterial = new THREE.MeshBasicMaterial( { color: 0x00ff00, wireframe: true } ); this.circle = new THREE.Mesh( circleGeometry, this.wireframeMaterial ); this.circle.rotation.x = Math.PI / - 3; /** Add Rectangles around the Circle */ this.rectangles = this.addRectanglesToCircle(30, 2); // 移除重复的loadTexture调用,避免重复加载 /** Debug Variables */ this.rectangles.forEach((rectangle) => { this.gui.add(rectangle.rotation, 'y').min(- 3).max(3).step(0.01).name('rotationY'); }); /** Grid & Axis Helper */ this.gridHelper = new THREE.GridHelper( 10, 10 ); this.axesHelper = new THREE.AxesHelper(5); this.scene.add( this.gridHelper, this.circle, this.axesHelper); this.camera.position.z = 5; this.renderer.render( this.scene, this.camera ); /** Orbit Controls */ this.controls = new OrbitControls(this.camera, this.renderer.domElement); this.animate(); } addRectanglesToCircle(numberOfRectangles, circleRadius) { const rectangleHeight = 0.5; const rectangleWidth = 1 const rectangleGeometry = new THREE.PlaneGeometry(rectangleWidth, rectangleHeight); const rectangleMaterial = new THREE.MeshBasicMaterial({ side: THREE.DoubleSide, map: this.texture || null }); const rectangles = []; for (let i = 0; i < numberOfRectangles; i++) { const theta = (i / numberOfRectangles) * 2 * Math.PI; const rectangle = new THREE.Mesh(rectangleGeometry, rectangleMaterial); rectangle.position.x = circleRadius * Math.sin(theta); rectangle.position.y = circleRadius * Math.cos(theta); rectangle.rotation.z = 0; rectangle.rotation.x = Math.PI / 2; rectangle.rotation.y = Math.PI / 2 - theta; this.circle.add(rectangle); rectangles.push(rectangle); } return rectangles; } animate() { requestAnimationFrame(this.animate.bind(this)); this.renderer.render(this.scene, this.camera); } }
注意事项
- 确保Active Storage配置正确,通过
rails_blob_url生成的URL在浏览器中能正常打开图片 - 若使用外部存储服务出现CORS问题,需在Rails配置中设置对应CORS规则
内容的提问来源于stack exchange,提问作者Inês Lucas
相关产品推荐
相关产品推荐

